# ================================================================== # Gossamer Threads Module Library - http://gossamer-threads.com/ # # GT::SQL::Search::NONINDEXED::Search # Author : Alex Krohn # CVS Info : 087,071,086,086,085 # $Id: Search.pm,v 1.30 2006/08/09 06:58:39 brewt Exp $ # # Copyright (c) 2004 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.30 $ =~ /(\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}); require GT::SQL::Condition; my $query_condition = new GT::SQL::Condition; # now handle the separate possibilities # the union my $union_cond = $self->_get_condition( $buckets->{keywords}, $buckets->{phrases} ); $query_condition->add(GT::SQL::Condition->new(@$union_cond, 'OR')) if $union_cond; # the intersect my $intersect_cond = $self->_get_condition( $buckets->{keywords_must}, $buckets->{phrases_must} ); $query_condition->add(GT::SQL::Condition->new(@$intersect_cond)) if $intersect_cond; # the disjoin my $disjoin_cond = $self->_get_condition( $buckets->{keywords_cannot}, $buckets->{phrases_cannot} ); $query_condition->add(GT::SQL::Condition->new(@$disjoin_cond, 'OR')->not) if $disjoin_cond; # now handle filters my $cols = $self->{'table'}->cols(); my %filters = map { (my $column = $_) =~ s/-[lg]t$//; exists $cols->{$column} ? ($_ => $input->{$_}) : () } 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 do that 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 = {}; while (my $result = $sth->fetchrow) { $results->{$result} = 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 my $sb = $self->clean_sb($input->{sb}, $input->{so}); my $offset = ( $input->{nh} - 1 ) * $input->{mh}; $tbl->select_options($sb) if ($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 @where = (); foreach my $cols ( @weights ) { push @where, [$cols, 'LIKE', "%$element%"]; # Condition does quoting by default. } push @cond, GT::SQL::Condition->new(@where, 'OR'); } @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) { push @terms, ($i++ % 2 ? $term : split ' ', $term); } for (my $i = 0; $i < @terms; $i++) { my $word = $terms[$i]; $word =~ s/^\s*|\s*$//g; next if ($word eq ''); if ($i < $#terms) { ($word eq '-') and ($word = '-' . $terms[++$i]); ($word eq '+') and ($word = '+' . $terms[++$i]); } $word =~ s/^([<>+-])//; my $mode = ($1 and $modes{$1} or 'can'); my $substring = ($word =~ s/\*$//) || 0; if ($word =~ /\s/) { $words->{$word} = { mode => $mode, phrase => 1, substring => $substring, keyword => 0, }; } elsif ($word) { $words->{$word} = { mode => $mode, phrase => 0, substring => $substring, keyword => 1, }; } } return $words; } } 1;