# ================================================================== # Gossamer Threads Module Library - http://gossamer-threads.com/ # # GT::SQL::Search::NONINDEXED::Search # Author : Alex Krohn # CVS Info : 087,068,083,090,083 # $Id: Search.pm,v 1.22 2002/01/23 22:22:59 alex Exp $ # # Copyright (c) 2001 Gossamer Threads Inc. All Rights Reserved. # ================================================================== # # Description: # Nonindex search system # package GT::SQL::Search::NONINDEXED::Search; # ================================================================== use strict; use vars qw/@ISA $ATTRIBS $VERSION $DEBUG/; use GT::SQL::Search::Base::Search; use GT::SQL::Condition; @ISA = qw( GT::SQL::Search::Base::Search ); $DEBUG = 0; $VERSION = sprintf "%d.%03d", q$Revision: 1.22 $ =~ /(\d+)\.(\d+)/; $ATTRIBS = { # parse based on latin characters latin_query_parse => 0 }; sub load { shift; return GT::SQL::Search::NONINDEXED::Search->new(@_) }; sub query { #-------------------------------------------------------------------------------- # Returns a sth based on a query # # Options: # - paging # mh : max hits # nh : number hit (or page of hits) # # - searching # ww : whole word # ma : 1 => OR match, 0 => AND match, undefined => QUERY # substring : search for substrings of words # bool : 'and' => and search, 'or' => or search, '' => regular query # query : the string of things to ask for # # - filtering # field_name : value # Find all rows with field_name = value # field_name : ">value" # Find all rows with field_name > value. # field_name : " value. # field_name-lt : value # Find all rows with field_name < value. # # Parameters: # ( $CGI ) : a single cgi object # ( $HASH ) : a hash of the parameters # my $self = shift; # find out what sort of a parameter we're dealing with my $input = $self->common_param(@_); # add additional parameters if required foreach my $parameter ( keys %{$ATTRIBS} ) { if ( not exists $input->{$parameter} ) { $input->{$parameter} = $self->{$parameter}; } } # parse query..., $self->debug( "Search Query: $$input{query}", 1 ) if ($self->{_debug}); my ( $query, $rejected ) = $self->_parse_query_string( $input->{'query'} ); $self->{'rejected_keywords'} = $rejected; # setup the additional input parameters $query = $self->_preset_options( $query, $input ); $self->debug( "Set the pre-options: ", $query ) if ($self->{_debug}); # now sort into distinct buckets my $buckets = GT::SQL::Search::Base::Search::_create_buckets( $query ); $self->debug_dumper( "Created Buckets for querying: ", $buckets ) if ($self->{_debug}); # now handle the separate possibilities my $union_cond = $self->_get_condition( $buckets->{keywords}, $buckets->{phrases} ); my $intersect_cond = $self->_get_condition( $buckets->{keywords_must}, $buckets->{phrases_must} ); my $disjoin_cond = $self->_get_condition( $buckets->{keywords_cannot}, $buckets->{phrases_cannot} ); # assmble the required stuff..., require GT::SQL::Condition; my $iterated_cond = undef; # the intersect my $intersect_condition = $intersect_cond ? GT::SQL::Condition->new( @$intersect_cond ) : undef; # the disjoin my $disjoin_condition = $disjoin_cond ? do { my $tmp = GT::SQL::Condition->new( @$disjoin_cond )->not(); $tmp->bool( 'or' ); $tmp; } : undef; # the union my $union_condition = $union_cond ? do { my $tmp = GT::SQL::Condition->new( @$union_cond ); $tmp->bool( 'or' ); $tmp; } : undef; # bring it all together my $query_condition = GT::SQL::Condition->new( ( $intersect_condition || () ), ( $disjoin_condition || () ), ( $union_condition || () ) ); # now handle filters..., my $cols = $self->{'table'}->cols(); my %filters = map { $_ => $input->{$_} } grep { my $tmp = $_; $tmp=~s/-(lt|gt)$//; exists $cols->{$tmp}; } keys %{$input}; # if there was no query nor filter return nothing. keys %$query or keys %filters or return $self->sth({}); if (keys %filters) { $self->debug( "Creating Filters: ", \%filters ) if ($self->{_debug}); $self->_add_filters( \%filters ); $query_condition = GT::SQL::Condition->new( keys %$query ? $query_condition : (), $self->{filter} ); } elsif ($self->{filter} and keys %{$self->{filter}} ) { $self->debug( "Filtering results", $self->{filter} ) if ($self->{_debug}); $query_condition = GT::SQL::Condition->new( keys %$query ? $query_condition : (), $self->{filter} ); } else { $self->debug( "No filters being used.") if ($self->{_debug}); } # now this query should probably clear the filters once it's been used, so i'll dothat here $self->{filter} = undef; my $tbl = $self->{table}; my ( $pk ) = $tbl->pk(); # now run through a callback function if needed. if ($self->{callback}) { # ... warning this slows things a heck of a lot. unless (ref $self->{callback} and ref $self->{callback} eq 'CODE') { return $self->error ('BADARGS', 'FATAL', "callback '$self->{callback}' must be a code ref!"); } my $sth = $tbl->select( [ $pk ], $query_condition ); my $results = {}; my $i = 1; while ( my $aref = $sth->fetchrow_arrayref() ) { $results->{$aref->[0]} = undef; } $self->debug_dumper ("Running results through callback. Had: " . scalar (keys %$results) . " results.", $results) if ($self->{_debug}); $results = $self->{callback}->($self, $results); $self->debug_dumper ("New result set: " . scalar (keys %$results) . " results.", $results) if ($self->{_debug}); $self->{rows} = scalar($results ? keys %{$results} : ()); return $self->sth( $results ); } # and now create a search sth object to handle all this $input->{nh} = (defined $input->{nh} and $input->{nh} =~ /^(\d+)$/) ? $1 : 1; $input->{mh} = (defined $input->{mh} and $input->{mh} =~ /^(\d+)$/) ? $1 : '25'; $input->{so} = (defined $input->{so} and $input->{so} =~ /^(asc(?:end)?|desc(?:end)?)$/i ? $1 : ''); # check that sb is not dangerous if ( my $sb = $input->{sb} ) { if ($sb !~ /^[\w\s,]+/) { $input->{sb} = ''; $input->{so} = ''; } if ($sb =~ /\s(?:ASC|DESC)/i) { $input->{so} = ''; } } my $offset = ( $input->{nh} - 1 ) * $input->{mh}; $tbl->select_options ("ORDER BY $input->{sb} $input->{so}") if ($input->{sb}); $tbl->select_options ("LIMIT $offset, $input->{mh}"); my $sth = $tbl->select( $query_condition ) or return; # so how many hits did we get? $self->{rows} = $sth->rows(); if (($input->{nh} > 1) or ($self->{rows} == $input->{mh})) { $self->{rows} = $tbl->count ($query_condition); } return $sth; } sub _get_condition { #------------------------------------------------------------------------------- my ( $self, $keywords, $phrases ) = @_; my @list = ( keys %$keywords, keys %$phrases ); my $tbl = $self->{table} or return $self->error( 'NODRIVER', 'FATAL' ); my @cond = (); my %tmp = $tbl->weight(); my @weights = keys %tmp or return; foreach my $element ( @list ) { my @tmp = (); foreach my $cols ( @weights ) { push @tmp, [ $cols, 'LIKE', "%$element%" ]; # Condition does quoting by default. } my $tmp_cond = GT::SQL::Condition->new(@tmp); $tmp_cond->bool('or'); push @cond, $tmp_cond; } @cond or return; return \@cond; } sub _parse_query_string { #------------------------------------------------------------ # Parses a query string '+foo -"bar this" alpha' into a hash of # words and modes. # my ($self, $text) = @_; my %modes = ( '+' => 'must', '-' => 'cannot', '<' => 'greater', '>' => 'less' ); # Latin will break up on actual words and punctuation. if ($self->{latin_query_parse}) { return $self->SUPER::_parse_query_string( $text ); } else { my $words = {}; my @terms; my $i = 0; foreach my $term (split /\"/, $text) { $i++ % 2 ? push @terms, $term : push @terms, split (/\s+/, $term); } for (my $i = 0; $i < @terms; $i++) { my $word = $terms[$i]; $word =~ s/^\s*|\s*$//g; next if ($word eq ''); ($word eq '-') and ($word = '-' . $terms[++$i]); ($word eq '+') and ($word = '+' . $terms[++$i]); $word =~ s,^([+-><]),,; my $mode = (defined $1 and defined $modes{$1}) ? $modes{$1} : 'can'; my $substring = ( $word =~ s,\*$,, ) || 0; if ($word =~ /\s/) { $words->{$word} = { mode => $mode, phrase => 1, substring => $substring, keyword => 0, }; } else { $words->{$word} = { mode => $mode, phrase => 0, substring => $substring, keyword => 1, }; } } return $words; } } 1;