-
Notifications
You must be signed in to change notification settings - Fork 182
[Search] Add ESQL version of full-text search + filtering tutorial #809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
0633e3f
wip
leemthompo 768cbe7
wip, move to standalone file
leemthompo 138a972
Merge branch 'main' into leemthompo/esql-search
leemthompo d2b56fa
Restore page accidentally updated with test
leemthompo bf9acf9
Fix links
leemthompo 52a4e99
Restore data ingestion step 😁
leemthompo 75d91af
Fix exact match query
leemthompo 535b7e5
Fix step numbers
leemthompo b76bb8b
Fix exact match query per original tutorial
leemthompo a0de185
fix query add annotation
leemthompo f65cd0a
fix link
leemthompo dcfc3b1
Add annotations to examples
leemthompo 51df3a5
actually fix link
leemthompo 6249e4a
Add semantic search stuff
leemthompo fdfe4c0
Cleanup language
leemthompo a4c533c
Fix links
leemthompo 22b1ad1
Clarify note about scoring
leemthompo b58a88e
Update title
leemthompo 23aaa29
tweak link path
leemthompo adc1782
delete repetition
leemthompo 30d0e9f
Add links in esql explore/analyze section
leemthompo 1c8da34
remove scoring caveat no longer true in 9.x
leemthompo 73d3d6c
Make API quickstarts more visible
leemthompo cc0fd52
Explain how to use commands in dev tools, simplify examples accordingly
leemthompo 69766d1
typos
leemthompo d3f36f9
Clean up annotations
leemthompo cda87c3
delete score from semantic query
leemthompo d3600f6
Merge branch 'main' into leemthompo/esql-search
leemthompo 6f4ba22
Apply suggestions from code review
leemthompo 4f4289f
update command
leemthompo 6b70da6
Merge branch 'main' into leemthompo/esql-search
leemthompo 8356100
add to landing page index
leemthompo b4b7048
fix link text, use variabesl
leemthompo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,358 @@ | ||
| --- | ||
| navigation_title: "Search and filter with ES|QL" | ||
| --- | ||
|
|
||
| # Tutorial: Search and filter with {{esql}} | ||
|
|
||
| :::{tip} | ||
| This tutorial presents examples in {{esql}} syntax. Refer to [the Query DSL version](querydsl-full-text-filter-tutorial.md) for the equivalent examples in Query DSL syntax. | ||
| ::: | ||
|
|
||
| This is a hands-on introduction to the basics of [full-text search](full-text.md) with Elasticsearch, also known as *lexical search*, and how to filter search results based on exact criteria. In this scenario, we're implementing a search function for a cooking blog. The blog contains recipes with various attributes including textual content, categorical data, and numerical ratings. | ||
|
|
||
| This tutorial covers lexical search, with a brief [introductory section on semantic search](#bonus-semantic-search-with-esql) at the end. | ||
|
|
||
| ## Requirements | ||
|
|
||
| You'll need a running {{es}} cluster, together with {{kib}} to use the Dev Tools API Console. Refer to [choose your deployment type](/deploy-manage/deploy.md#choosing-your-deployment-type) for deployment options. | ||
|
|
||
| Want to get started quickly? Run the following command in your terminal to set up a [single-node local cluster in Docker](get-started.md): | ||
|
|
||
| ```sh | ||
| curl -fsSL https://elastic.co/start-local | sh | ||
| ``` | ||
|
|
||
| ## Running {{esql}} queries | ||
|
|
||
| In this tutorial, you'll see {{esql}} examples in the following format: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog | ||
| | WHERE description:"fluffy pancakes" | ||
| | LIMIT 1000 | ||
| ``` | ||
|
|
||
| If you want to run these queries in the [Dev Tools Console](/explore-analyze/query-filter/languages/esql-rest.md#esql-kibana-console), you'll need to use the following syntax: | ||
|
|
||
| ```console | ||
| POST /_query?format=txt | ||
| { | ||
| "query": """ | ||
| FROM cooking_blog | ||
| | WHERE description:"fluffy pancakes" | ||
| | LIMIT 1000 | ||
| """ | ||
| } | ||
| ``` | ||
|
|
||
| If you'd prefer to use your favorite programming language, refer to [Client libraries](/solutions/search/site-or-app/clients.md) for a list of official and community-supported clients. | ||
|
|
||
| ## Step 1: Create an index | ||
|
|
||
| Create the `cooking_blog` index to get started: | ||
|
|
||
| ```console | ||
| PUT /cooking_blog | ||
| ``` | ||
|
|
||
| Now define the mappings for the index: | ||
|
|
||
| ```console | ||
| PUT /cooking_blog/_mapping | ||
| { | ||
| "properties": { | ||
| "title": { | ||
| "type": "text", | ||
| "analyzer": "standard", <1> | ||
| "fields": { <2> | ||
| "keyword": { | ||
| "type": "keyword", | ||
| "ignore_above": 256 <3> | ||
| } | ||
| } | ||
| }, | ||
| "description": { | ||
| "type": "text", | ||
| "fields": { | ||
| "keyword": { | ||
| "type": "keyword" | ||
| } | ||
| } | ||
| }, | ||
| "author": { | ||
| "type": "text", | ||
| "fields": { | ||
| "keyword": { | ||
| "type": "keyword" | ||
| } | ||
| } | ||
| }, | ||
| "date": { | ||
| "type": "date", | ||
| "format": "yyyy-MM-dd" | ||
| }, | ||
| "category": { | ||
| "type": "text", | ||
| "fields": { | ||
| "keyword": { | ||
| "type": "keyword" | ||
| } | ||
| } | ||
| }, | ||
| "tags": { | ||
| "type": "text", | ||
| "fields": { | ||
| "keyword": { | ||
| "type": "keyword" | ||
| } | ||
| } | ||
| }, | ||
| "rating": { | ||
| "type": "float" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| 1. The `standard` analyzer is used by default for `text` fields if an `analyzer` isn't specified. It's included here for demonstration purposes. | ||
| 2. [Multi-fields](elasticsearch://reference/elasticsearch/mapping-reference/multi-fields.md) are used here to index `text` fields as both `text` and `keyword` [data types](elasticsearch://reference/elasticsearch/mapping-reference/field-data-types.md). This enables both full-text search and exact matching/filtering on the same field. Note that if you used [dynamic mapping](../../manage-data/data-store/mapping/dynamic-field-mapping.md), these multi-fields would be created automatically. | ||
| 3. The [`ignore_above` parameter](elasticsearch://reference/elasticsearch/mapping-reference/ignore-above.md) prevents indexing values longer than 256 characters in the `keyword` field. Again this is the default value, but it's included here for demonstration purposes. It helps to save disk space and avoid potential issues with Lucene's term byte-length limit. | ||
|
|
||
| ::::{tip} | ||
| Full-text search is powered by [text analysis](full-text/text-analysis-during-search.md). Text analysis normalizes and standardizes text data so it can be efficiently stored in an inverted index and searched in near real-time. Analysis happens at both [index and search time](../../manage-data/data-store/text-analysis/index-search-analysis.md). This tutorial won't cover analysis in detail, but it's important to understand how text is processed to create effective search queries. | ||
| :::: | ||
|
|
||
| ## Step 2: Add sample blog posts to your index [full-text-filter-tutorial-index-data] | ||
|
|
||
| Now you’ll need to index some example blog posts using the [Bulk API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-put-settings). Note that `text` fields are analyzed and multi-fields are generated at index time. | ||
|
|
||
| ```console | ||
| POST /cooking_blog/_bulk?refresh=wait_for | ||
| {"index":{"_id":"1"}} | ||
| {"title":"Perfect Pancakes: A Fluffy Breakfast Delight","description":"Learn the secrets to making the fluffiest pancakes, so amazing you won't believe your tastebuds. This recipe uses buttermilk and a special folding technique to create light, airy pancakes that are perfect for lazy Sunday mornings.","author":"Maria Rodriguez","date":"2023-05-01","category":"Breakfast","tags":["pancakes","breakfast","easy recipes"],"rating":4.8} | ||
| {"index":{"_id":"2"}} | ||
| {"title":"Spicy Thai Green Curry: A Vegetarian Adventure","description":"Dive into the flavors of Thailand with this vibrant green curry. Packed with vegetables and aromatic herbs, this dish is both healthy and satisfying. Don't worry about the heat - you can easily adjust the spice level to your liking.","author":"Liam Chen","date":"2023-05-05","category":"Main Course","tags":["thai","vegetarian","curry","spicy"],"rating":4.6} | ||
| {"index":{"_id":"3"}} | ||
| {"title":"Classic Beef Stroganoff: A Creamy Comfort Food","description":"Indulge in this rich and creamy beef stroganoff. Tender strips of beef in a savory mushroom sauce, served over a bed of egg noodles. It's the ultimate comfort food for chilly evenings.","author":"Emma Watson","date":"2023-05-10","category":"Main Course","tags":["beef","pasta","comfort food"],"rating":4.7} | ||
| {"index":{"_id":"4"}} | ||
| {"title":"Vegan Chocolate Avocado Mousse","description":"Discover the magic of avocado in this rich, vegan chocolate mousse. Creamy, indulgent, and secretly healthy, it's the perfect guilt-free dessert for chocolate lovers.","author":"Alex Green","date":"2023-05-15","category":"Dessert","tags":["vegan","chocolate","avocado","healthy dessert"],"rating":4.5} | ||
| {"index":{"_id":"5"}} | ||
| {"title":"Crispy Oven-Fried Chicken","description":"Get that perfect crunch without the deep fryer! This oven-fried chicken recipe delivers crispy, juicy results every time. A healthier take on the classic comfort food.","author":"Maria Rodriguez","date":"2023-05-20","category":"Main Course","tags":["chicken","oven-fried","healthy"],"rating":4.9} | ||
| ``` | ||
|
|
||
| ## Step 3: Perform basic full-text searches | ||
|
|
||
| Full-text search involves executing text-based queries across one or more document fields. These queries calculate a relevance score for each matching document, based on how closely the document's content aligns with the search terms. Elasticsearch offers various query types, each with its own method for matching text and relevance scoring. | ||
|
|
||
| :::{tip} | ||
| {{esql}} provides two ways to perform full-text searches: | ||
|
|
||
| 1. Full match function syntax: `match(field, "search terms")` | ||
carlosdelest marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 1. Compact syntax using the match operator "`:`": `field:"search terms"` | ||
|
|
||
| Both are equivalent and can be used interchangeably. The compact syntax is more concise, while the function syntax allows for more configuration options. We'll use the compact syntax in most examples for brevity. | ||
| ::: | ||
|
|
||
| ### Basic full-text query | ||
|
|
||
| Here's how to search the `description` field for "fluffy pancakes": | ||
|
|
||
| ```esql | ||
| FROM cooking_blog # Specify the index to search | ||
| | WHERE description:"fluffy pancakes" # Full-text search with OR logic by default | ||
| | LIMIT 1000 # Return up to 1000 results | ||
| ``` | ||
|
|
||
| By default, like the Query DSL `match` query, {{esql}} uses `OR` logic between terms. This means it will match documents that contain either "fluffy" or "pancakes", or both, in the description field. | ||
|
|
||
| :::{tip} | ||
| You can control which fields to include in the response using the `KEEP` command: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog | ||
| | WHERE description:"fluffy pancakes" | ||
| | KEEP title, description, rating # Select only specific fields to include in response | ||
| | LIMIT 1000 | ||
| ``` | ||
| ::: | ||
|
|
||
| ### Require all terms in a match query | ||
|
|
||
| Sometimes you need to require that all search terms appear in the matching documents. Here's how to do that using the function syntax with the `operator` parameter: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog | ||
| | WHERE match(description, "fluffy pancakes", {"operator": "AND"}) # Require ALL terms to match | ||
| | LIMIT 1000 | ||
| ``` | ||
|
|
||
| This stricter search returns *zero hits* on our sample data, as no document contains both "fluffy" and "pancakes" in the description. | ||
|
|
||
| ### Specify a minimum number of terms to match | ||
|
|
||
| Sometimes requiring all terms is too strict, but the default OR behavior is too lenient. You can specify a minimum number of terms that must match: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog | ||
| | WHERE match(title, "fluffy pancakes breakfast", {"minimum_should_match": 2}) | ||
| | LIMIT 1000 | ||
| ``` | ||
|
|
||
| This query searches the title field to match at least 2 of the 3 terms: "fluffy", "pancakes", or "breakfast". | ||
|
|
||
| ## Step 4: Search across multiple fields at once | ||
|
|
||
| When users enter a search query, they often don't know (or care) whether their search terms appear in a specific field. {{esql}} provides ways to search across multiple fields simultaneously: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog | ||
| | WHERE title:"vegetarian curry" OR description:"vegetarian curry" OR tags:"vegetarian curry" | ||
| | LIMIT 1000 | ||
| ``` | ||
|
|
||
| This query searches for "vegetarian curry" across the title, description, and tags fields. Each field is treated with equal importance. | ||
|
|
||
| However, in many cases, matches in certain fields (like the title) might be more relevant than others. We can adjust the importance of each field using scoring: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog METADATA _score # Request _score metadata for relevance-based results | ||
| | WHERE match(title, "vegetarian curry", {"boost": 2.0}) # Title matches are twice as important | ||
| OR match(description, "vegetarian curry") | ||
| OR match(tags, "vegetarian curry") | ||
| | KEEP title, description, tags, _score # Include relevance score in results | ||
| | SORT _score DESC # You must explicitly sort by `_score` to see relevance-based results | ||
| | LIMIT 1000 | ||
| ``` | ||
|
|
||
| :::{tip} | ||
| When working with relevance scoring in ES|QL, it's important to understand how `_score` works. If you don't include `METADATA _score` in your query, you're only performing filtering operations with no relevance calculation. | ||
|
|
||
| When you include `METADATA _score`, only search functions contribute to the relevance score. Filtering operations (like range conditions and exact matches) don't affect the score. | ||
leemthompo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Remember that including `METADATA _score` doesn't automatically sort your results by relevance. You must explicitly use `SORT _score DESC` or `SORT _score ASC` to order your results by relevance. | ||
| ::: | ||
|
|
||
| ## Step 5: Filter and find exact matches | ||
|
|
||
| Filtering allows you to narrow down your search results based on exact criteria. Unlike full-text searches, filters are binary (yes/no) and do not affect the relevance score. Filters execute faster than queries because excluded results don't need to be scored. | ||
|
|
||
| ```esql | ||
| FROM cooking_blog | ||
| | WHERE category.keyword == "Breakfast" # Exact match using keyword field(case-sensitive) | ||
| | KEEP title, author, rating, tags | ||
| | SORT rating DESC | ||
| | LIMIT 1000 | ||
| ``` | ||
|
|
||
| Note the use of `category.keyword` here. This refers to the [`keyword`](elasticsearch://reference/elasticsearch/mapping-reference/keyword.md) multi-field of the `category` field, ensuring an exact, case-sensitive match. | ||
|
|
||
| ### Search for posts within a date range | ||
|
|
||
| Often users want to find content published within a specific time frame: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog | ||
| | WHERE date >= "2023-05-01" AND date <= "2023-05-31" # Inclusive date range filter | ||
| | KEEP title, author, date, rating | ||
| | LIMIT 1000 | ||
| ``` | ||
|
|
||
| ### Find exact matches | ||
|
|
||
| Sometimes users want to search for exact terms to eliminate ambiguity in their search results: | ||
leemthompo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ```esql | ||
| FROM cooking_blog | ||
| | WHERE author.keyword == "Maria Rodriguez" # Exact match on author | ||
| | KEEP title, author, rating, tags | ||
| | SORT rating DESC | ||
| | LIMIT 1000 | ||
| ``` | ||
|
|
||
| Like the `term` query in Query DSL, this has zero flexibility and is case-sensitive. | ||
|
|
||
| ## Step 6: Combine multiple search criteria | ||
|
|
||
| Complex searches often require combining multiple search criteria: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog METADATA _score | ||
| | WHERE rating >= 4.5 # Numerical filter | ||
| AND NOT category.keyword == "Dessert" # Exclusion filter | ||
| AND (title:"curry spicy" OR description:"curry spicy") # Full-text search in multiple fields | ||
| | SORT _score DESC | ||
| | KEEP title, author, rating, tags, description | ||
| | LIMIT 1000 | ||
| ``` | ||
|
|
||
| ### Combine relevance scoring with custom criteria | ||
|
|
||
| For more complex relevance scoring with combined criteria, you can use the `EVAL` command to calculate custom scores: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog METADATA _score | ||
| | EVAL tags_concat = MV_CONCAT(tags.keyword, ",") # Convert multi-value field to string | ||
| | WHERE tags_concat LIKE "*vegetarian*" AND rating >= 4.5 # Wildcard pattern matching | ||
| | WHERE match(title, "curry spicy", {"boost": 2.0}) OR match(description, "curry spicy") | ||
leemthompo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| | EVAL category_boost = CASE(category.keyword == "Main Course", 1.0, 0.0) # Conditional boost | ||
| | EVAL date_boost = CASE(DATE_DIFF("month", date, NOW()) <= 1, 0.5, 0.0) # Boost recent content | ||
| | EVAL custom_score = _score + category_boost + date_boost # Combine scores | ||
| | WHERE NOT category.keyword == "Dessert" | ||
|
||
| | WHERE custom_score > 0 # Filter based on custom score | ||
| | SORT custom_score DESC | ||
| | LIMIT 1000 | ||
|
|
||
| ``` | ||
|
|
||
| ## Bonus: Semantic search with ES|QL | ||
|
|
||
| ES|QL also supports semantic search when your mappings include fields of the [`semantic_text`](elasticsearch://reference/elasticsearch/mapping-reference/semantic-text.md) type. This example mapping update adds a new field called `semantic_description` with the type `semantic_text`: | ||
|
|
||
| ```console | ||
| PUT /cooking_blog/_mapping | ||
| { | ||
| "properties": { | ||
| "semantic_description": { | ||
| "type": "semantic_text" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Next, index a document with content into the new field: | ||
|
|
||
| ```console | ||
| POST /cooking_blog/_doc | ||
| { | ||
| "title": "Mediterranean Quinoa Bowl", | ||
| "semantic_description": "A protein-rich bowl with quinoa, chickpeas, fresh vegetables, and herbs. This nutritious Mediterranean-inspired dish is easy to prepare and perfect for a quick, healthy dinner.", | ||
| "author": "Jamie Oliver", | ||
| "date": "2023-06-01", | ||
| "category": "Main Course", | ||
| "tags": ["vegetarian", "healthy", "mediterranean", "quinoa"], | ||
| "rating": 4.7 | ||
| } | ||
| ``` | ||
|
|
||
| Once the document has been processed by the underlying model running on the inference endpoint, you can perform semantic searches. Here's an example natural language query against the `semantic_description` field: | ||
|
|
||
| ```esql | ||
| FROM cooking_blog | ||
| | WHERE semantic_description:"What are some easy to prepare but nutritious plant-based meals?" | ||
| | LIMIT 5 | ||
|
|
||
| ``` | ||
|
|
||
| :::{tip} | ||
| Follow this [tutorial](/solutions/search/semantic-search/semantic-search-semantic-text.md) if you'd like to test out the semantic search workflow against a large dataset. | ||
| ::: | ||
|
|
||
| ## Learn more | ||
|
|
||
| This tutorial introduced the basics of search and filtering in {{esql}}. Building a real-world search experience requires understanding many more advanced concepts and techniques. Here are some resources once you're ready to dive deeper: | ||
|
|
||
| - [Full-text search](full-text.md): Learn about the core components of full-text search in Elasticsearch. | ||
| - [Text analysis](full-text/text-analysis-during-search.md): Understand how text is processed for full-text search. | ||
| - [{{esql}} search functions](elasticsearch://reference/query-languages/esql/esql-functions-operators.md#esql-search-functions): Explore the full list of search functions available in {{esql}}. | ||
| - [Semantic search](/solutions/search/semantic-search.md): Understand your various options for semantic search in Elasticsearch. | ||
| - [The `semantic_text` workflow](/solutions/search/semantic-search.md#_semantic_text_workflow): Learn how to use the `semantic_text` field type for semantic search. This is the recommended approach for most users looking to perform semantic search in {{es}}, because it abstracts away the complexity of setting up inference endpoints and models. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we focus primarily on lexical search. where do you think we should add a note on semantic search with
match? If possible I would add a section on semantic search in this guide - can also be done as a follow up.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ioanatia happy to do it here. The first pass was just duplicating the existing tutorial but I agree that given the simplicity of working with semantic_text in the
matchworkflow, why not add that here too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
6249e4a