Query Parser merged copy / non-dynamic filters
authorBill Erickson <berick@esilibrary.com>
Mon, 21 May 2012 14:32:56 +0000 (10:32 -0400)
committerMike Rylander <mrylander@gmail.com>
Tue, 22 May 2012 19:01:23 +0000 (15:01 -0400)
When more than one copy-level filter (e.g. locations()) is used in a
query-parser query, all but the first are ignored.  The goal of this
work is to compress multiple copy-level filters into a single filter
that represents the full collection.  Wherever possible, boolean
structures will be honored, to the extent possible for post-search
copy-level filters.

Examples:

concerto locations(1,2) locations(3,4)

==> concerto locations(1,2,3,4)

( concerto locations(3,4,5) ) || (piano locations(3,5,7) )

==> ( concerto || piano ) locations(3,4,5,6,7)

( concerto locations(3,4,5) ) && ( piano locations(3,5,7) )

==> concerto piano locations(3,5)

Note, in the last 2 examples, the final query does not exactly match the
original.  This is because copy-level filters are applied after the
initial search and cannot be executed as part of the  nested query.  The
best we can do is create a representation of the final copy-level
filter, based on the nesting, and apply it to the final result of the
nested search.

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm

index d5e12a4..4cb51ed 100644 (file)
@@ -971,11 +971,80 @@ sub new_filter {
     return $node;
 }
 
+
+sub _merge_filters {
+    my $left_filter = shift;
+    my $right_filter = shift;
+    my $join = shift;
+
+    return undef unless $left_filter or $right_filter;
+    return $right_filter unless $left_filter;
+    return $left_filter unless $right_filter;
+
+    my $args = $left_filter->{args} || [];
+
+    if ($join eq '|') {
+        push(@$args, @{$right_filter->{args}});
+
+    } else {
+        # find the intersect values
+        my %new_vals;
+        map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
+        $args = [ grep { $new_vals{$_} } @$args ];
+    }
+
+    $left_filter->{args} = $args;
+    return $left_filter;
+}
+
+sub collapse_filters {
+    my $self = shift;
+    my $name = shift;
+
+    # start by merging any filters at this level.
+    # like-level filters are always ORed together
+
+    my $cur_filter;
+    my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
+    if (@cur_filters) {
+        $cur_filter = shift @cur_filters;
+        my $args = $cur_filter->{args} || [];
+        $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
+    }
+
+    # next gather the collapsed filters from sub-plans and 
+    # merge them with our own
+
+    my @subquery = @{$self->{query}};
+
+    while (@subquery) {
+        my $blob = shift @subquery;
+        shift @subquery; # joiner
+        next unless $blob->isa('QueryParser::query_plan');
+        my $sub_filter = $blob->collapse_filters($name);
+        $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
+    }
+
+    if ($self->QueryParser->debug) {
+        my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
+        warn "collapse_filters($name) => [@args]\n";
+    }
+
+    return $cur_filter;
+}
+
 sub find_filter {
     my $self = shift;
     my $needle = shift;;
     return undef unless ($needle);
-    return grep { $_->name eq $needle } @{ $self->filters };
+
+    my $filter = $self->collapse_filters($needle);
+
+    warn "find_filter($needle) => " . 
+        (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n" 
+        if $self->QueryParser->debug;
+
+    return $filter ? ($filter) : ();
 }
 
 sub find_modifier {
@@ -1109,7 +1178,6 @@ sub add_filter {
     my $filter = shift;
 
     $self->{filters} ||= [];
-    $self->{filters} = [ grep {$_->name ne $filter->name} @{$self->{filters}} ];
 
     push(@{$self->{filters}}, $filter);