Lucy::...torial(3pm)
β¨ 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
- fedora 43: perl-Lucy 0.6.2-26.fc43 Β· Apache-2.0 AND MIT AND Unicode-DFS-2015
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms