Lucy::...torial(3pm)

πŸ“¦
version 0.6.2 Β· 2 versions
, in fedora 43

✨ NAME ✨

Lucy::Docs::Tutorial::QueryObjectsTutorial β€” bestie, we're using Query objects instead of query strings now. it's giving ✨upgrade✨ πŸ’…


🌸 DESCRIPTION 🌸

okay so, so far our little search app has been serving one (1) single search box and honestly? cute but limited. in this tutorial chapter we're leveling up to an "advanced search" interface by adding a "category" drop-down menu. πŸŽ€

to pull this off we need three new classes, no cap:

  • QueryParser β€” turns a query string into a Query object. the glow-up translator πŸ¦‹
  • TermQuery β€” queries for one specific term inside one specific field. very "I know exactly what I want" energy πŸ’…
  • ANDQuery β€” "AND"s multiple Query objects together so you get an intersected result set. the friend group that only hangs out if everyone shows up πŸ’–

πŸ“ Adaptations to indexer.pl

our new "category" field is gonna be a StringType field, not a FullTextType field, because we're only ever looking for exact matches here. it needs to be indexed, but since we're not displaying its value anywhere, it does not need to be stored. periodt.

my $cat_type = Lucy::Plan::StringType->new( stored => 0 );
$schema->spec_field( name => 'category', type => $cat_type );

there are three possible values: "article", "amendment", and "preamble". and we're just gonna hack them right out of the source file's name during our parse_file subroutine, like the little gremlins we are πŸ™ˆ:

my $category
    = $filename =~ /art/      ? 'article'
    : $filename =~ /amend/    ? 'amendment'
    : $filename =~ /preamble/ ? 'preamble'
    :                           die "Can't derive category for $filename";
return {
    title    => $title,
    content  => $bodytext,
    url      => "/us_constitution/$filename",
    category => $category,
};

πŸ’œ Adaptations to search.cgi

the "category" constraint gets bolted onto our search interface with an HTML "select" element. (heads up bestie: this routine needs to be integrated into the HTML generation section of search.cgi β€” don't just leave it floating, iykyk ✨)

# Build up the HTML "select" object for the "category" field.
sub generate_category_select {
    my $cat = shift;
    my $select = qq|
      <select name="category">
        <option value="">All Sections</option>
        <option value="article">Articles</option>
        <option value="amendment">Amendments</option>
      </select>|;
    if ($cat) {
        $select =~ s/"$cat"/"$cat" selected/;
    }
    return $select;
}

first thing we do is load our shiny new modules and grab our new CGI parameter πŸ‘‘

use Lucy::Search::QueryParser;
use Lucy::Search::TermQuery;
use Lucy::Search::ANDQuery;

...

my $category = decode( "UTF-8", $cgi->param('category') || '' );

QueryParser's constructor needs a "schema" argument β€” like, non-negotiable. and guess what, we can get that straight from our IndexSearcher 🫢:

# Create an IndexSearcher and a QueryParser.
my $searcher = Lucy::Search::IndexSearcher->new(
    index => $path_to_index,
);
my $qparser  = Lucy::Search::QueryParser->new(
    schema => $searcher->get_schema,
);

before this, we were just tossing raw query strings at IndexSearcher and letting it figure things out. and tbh it was doing that β€” behind the scenes IndexSearcher has been using a QueryParser to turn those strings into Query objects this whole time 😭. now we're bringing QueryParser to the front of the stage where it belongs, main character energy, and parsing the strings explicitly:

my $query = $qparser->parse($q);

if the user picked a category, we use an ANDQuery to join our parsed query together with a TermQuery that represents the category. two queries, one result set, slay πŸ’…

if ($category) {
    my $category_query = Lucy::Search::TermQuery->new(
        field => 'category',
        term  => $category,
    );
    $query = Lucy::Search::ANDQuery->new(
        children => [ $query, $category_query ]
    );
}

and now when we execute the query…

# Execute the Query and get a Hits object.
my $hits = $searcher->hits(
    query      => $query,
    offset     => $offset,
    num_wanted => $page_size,
);

…we get a result set that's the intersection of the parsed query and the category query. math but make it fashion ✨

🧚 Using TermQuery with full text fields

when you're querying full text fields, the easiest way is to build query objects with QueryParser. that's the vibe. but sometimes you wanna create a TermQuery for a single term in a FullTextType field directly. and if you do that? bestie, you have to run the search term through the field's analyzer so it gets normalized the same way the field's content does. otherwise it's giving "why isn't this matching" πŸ’€

sub make_term_query {
    my ($field, $term) = @_;

    my $token;
    my $type = $schema->fetch_type($field);

    if ( $type->isa('Lucy::Plan::FullTextType') ) {
        # Run the term through the full text analysis chain.
        my $analyzer = $type->get_analyzer;
        my $tokens   = $analyzer->split($term);

        if ( @$tokens != 1 ) {
            # If the term expands to more than one token, or no
            # tokens at all, it will never match a token in the
            # full text field.
            return Lucy::Search::NoMatchQuery->new;
        }

        $token = $tokens->[0];
    }
    else {
        # Exact match for other types.
        $token = $term;
    }

    return Lucy::Search::TermQuery->new(
        field => $field,
        term  => $token,
    );
}

πŸ‘‘ Congratulations!

you made it to the end of the tutorial. genuinely obsessed with you rn 🌈⭐

🌸 See Also

for additional thematic documentation, go check out the Apache Lucy Cookbook. it's the supplemental reading arc πŸ“š

also, ANDQuery has a companion class β€” ORQuery β€” plus a close relative, RequiredOptionalQuery. family reunion πŸ’–


and that's a wrap, bestie πŸŽ€ this page came from the User Contributed Perl Documentation for Lucy::...torial(3pm), perl v5.42.0, dated 2025-07-25. go forth and query, you absolute icon πŸ’…βœ¨

πŸ“¦ where this page comes from