In this lesson, you will develop a multi-stage MongoDB aggregation pipeline. You will discover how to use metadata to refine and limit the search results returned from database operation, enhancing efficiency and relevancy. All right, let's have some fun. Metadata is simply additional information about a specific data. It's meant to supplement the key data point and provide more context. For example, imagine an image of the Mona Lisa. The image is a key data, which can be the vector embedding of the image itself. Other data such as the title, artist, name, location, and more can be added to the image embedded as metadata. Both the key data and the accompanying metadata can be stored in MongoDB as a single document. By pairing the vector embedded with metadata, the overall image data becomes more informative and useful. Let's understand how metadata can be useful within LLM applications and RAG design pattern, Metadata can be used to add additional context to the embedding data for improved relevance and understanding. Metadata also improves the relevance of vector search queries by enabling filtering and sorting based on data attributed to the embedding data. This can reduce the scope of the vector search query operation or its result. In order to streamline the results of vector search operation to improve relevance, you will use metadata within filtering stages compose together with a vector search stage in an aggregation pipeline. You will use MongoDB's aggregation pipeline to create composable queries, which makes it easier to think of and implement complex queries. Creating an aggregation pipeline with filtering stages enables database operation to produce more relevant results. Let's see how. An example of one of the filtering technique is post-filtering. This is where a vector search stage is conducted and the results are reduced based on certain criteria are referred to as filter. Imagine you have a user query that has certain keywords such as seaside and restaurant, but it also contains constraints such as specified quantities of room and capacity requirement. You can examine how a post filtering process will occur by first starting with the full data set, then applying a vector search operation on the full data set to get results that are semantically similar to the user query. Then, in a post-filtering operation, you apply the filter stage after the vector search stage to further reduce the return result based on a specified criteria. Now, let's see another filtering technique known as pre-filtering. Using pre-filtering technique within vector search can produce different results at the end of the database operation. Let's see how. Start with using the same user query and the same data set. But this time the filter operation or stage is applied to the data set to remove results that don't meet the filter criteria. After this initial reduction, then vector search is applied on the filter stage result. The key difference you takeaway is that pre-filtering involves applying a filter to the data set before conducting the vector search. This approach reduces a subset of data that the vector search will process for similarity measurement. But one key takeaway from post-filtering and the pre-filtering technique is that post-filtering might reduce the amount of document you use for semantic similarity with a user query vector. Which means there could be potential loss of information or records that could be semantically similar to the user query, but are not returned as a result of the filter stage. Now let's see what you're going to build in the coding section. You're going to set a simple RAG pipeline, but then you add a post-filtering stage and observe the result. Which you would then under the user query accordingly. You then, will add a pre-filter stage to observe the different result from a post-filter to a pre-filter, and you will handle the user query accordingly. Let's code. You'll start by importing custom details. This module has been created to streamline some of the code you used in lesson one. You'll notice where custom_utils methods are used and an explanation will be provided. The first step you will take is to load the data. For data loading, you're loading the same data you used in lesson one. To load your data and ensure it conforms to the appropriate model you saw in lesson one, you will use the process records within the custom utils module. This method takes in the data set with loaded and then conforms each data point to the model specified in lesson one. The result of this operation is a Python list containing data points, where each data point is an Airbnb listing that conforms to the specified model. The next step is to make a connection to the database. Connecting to the database has been moved to the costume utils module. Calling the function connect to database will execute the process of connecting to the MongoDB cluster and obtaining objects representing the database and collection. You will unpack the return results which will provide you with a database object and the collection object. This is the same step you did in lesson one. Here, we've streamlined it to a simple function call. The next step is to ensure you are working with a clean collection. In lesson one you ingested some data into your MongoDB collection. In this lesson, running the delete menu from the collection object will result in deletion of the data ingested into the collection in lesson one. You will observe the number of data it has been deleted by the delete many operation. Here, we have 100 data points deleted. The next step is to ingest the data. In this step, you ingest all the listing data that was created earlier into your MongoDB collection by calling the insert many method on the collection object and passing in the listings as its argument, the ingestion process will begin. On the screen, you will see a print statement indicating that the ingestion process is completed. The next step is to create a vector search index. You created a vector search index in lesson one. In lesson two, this extensive code has been streamlined into a code within a custom_utils modules named "Set up Vector Search Index". This will take in a collection for which to create the index for. This is an expected result. You've gotten a duplicated index. Recall, you created an index in lesson one already. Don't worry, you can carry on with the lesson. The next step is to compose the vector search query. This is a step you also took in lesson one, and it was one of the most important part of the process. You would just go over the code again. It's still the same as lesson one. Recall, the vector search function takes in a few arguments of the user query, the database, and the collection converts the user query into an embedding, uses the embedding within a vector search operation, and creates a pipeline with the vector search stage and any additional stage, and calls the aggregate method on the pipeline to get the result. You also print out the execution time of the vector search stage, and finally the result of the database operation is returned by the vector search function. The next step is to handle user queries. The code for this lesson is similar to the previous lesson. The only difference is we have different attributes and you are using a custom utils to get the address module. In a similar fashion to the previous lesson, the handle user query is pretty much the same. The handle user query will take in the arguments which are the the user query, the database object, and the collection object. Also some defaults argument of the stages and the vector index name. You start the process by getting some search results from the vector search operation. The handle user query also conforms the results from a database operation to the search results item model specified in the previous cell. The search results are converted into a dictionary and passed into the model, with the query as additional context. Finally, you extract the system response and print it out on a notebook in a structured manner. The handle user query returns the final system response. Now, the fun begins where you implement, post-filtering process that is conducted after the vector search operation. In this cell, we are specifying the path address.country. Essentially, the filtering you'll be conducting will mimic a scenario where your app user only wants to see listings in the United States. First, you specify the path of where the country is located within the document. This is located in the address field, specifically in the country field. You specify the path to this and assign it to the variable search path. To create a match stage, you use the dollar operator with the specific fields search path, which takes in the string to search for. You will notice you're also adding another limitation or filtering of the document based on the capacity a listing can accommodate. In real life scenario, there are situations where a listing would only want to take a certain amount of people, or need a certain amount of people. You can mimic this in your query by specifying conditional operators or conditional statements within your query. For this filter or match stage, you will limit the documents return to listings that can only accommodate greater than one person or less than five persons, and this is the filtering condition you will be adding after the vector search operation. Finally, you can pass in the match stage and assign it to a variable called "Additional" stage. This cell contains the user query. It's similar to the same user query in the previous lesson where the user wants to stay in a place that's warm and friendly. That's not too far from restaurants. The outputs will also be similar where we see, user query and system response and the documents that were used for additional context in the table structure. You will observe that the documents returned are limited to the United States because you are passing the additional stage that contains that match filter. This is the prompt you're passing to the system, you also pass in the additional stage, which is a match stage created in the previous cell. You will observe that the vector search operation took a fraction of a millisecond. The system recommended easy one bedroom in Chelsea. One thing I want you to do is to pause this video and observe the location of the documents, users additional contacts. You will notice they're all from the United States. You also observe that the accommodates for each data point is between the numbers one and five. Here, you can see that the locations are all from the United States. Let's begin with the second half of the fun, which is adding a pre-filter to the vector search operation. Here, you are creating the filter before the vector search operation is conducted. To conduct pre-filtering efficiently, in this cell you are creating a new vector search index. This is similar to the vector such index created in the previous lesson. The difference here is, we are creating the index with the fields you are going to be filtering the results of the data operation on. These are specifically the accommodates field that was added in the match stage previously under bedrooms. It's important to create an efficient vector search index for your collection. This allows retrieval of information all documents to be performant and not take too long. As previously done, you need to name your vector search index. You will call this specific index "vector index with filter". Distinguishing it from the previous index created in a previous lesson. To create the index, you will call the create such index function on the collection object, which will return the created index. Again, you be defining a vector search function. But this vector, search function is going to be different. Let me explain how. We have a similar vector search function you implemented in a previous cell. The difference is, in the vector stage you are adding a filter field. This filter field conducts the filter operation before conducting the vector search operation. This filter is similar to what you implemented in a match stage, where you specified a condition or filter based on the accommodates field. In this filter, you are adding an additional step to limit the results that are considered for vector search operation to ones that have bedrooms that are less than or equal to seven. With MongoDB, you can add conditional operation and let the database handle the logic. Using the and dollar operator. You can pass in conditions that limit the documents returned from database operations. This is essentially the pre-filter in step. And then the vector search process will be conducted. The rest of the code remains the same. Where you create the pipeline, and then execute the pipeline to obtain the result. You will also view the execution time for the vector search query. The final step is to handle the user query, where you will be using the same query as specified previously. But the difference here is in the handle user query and what you pass in as arguments into this function. Observe there is no additional stage you're passing into the handle user query to be considered for the database operation. What you will pass in is a new vector search index that was created with a filter, and also the vector search function now contains a filter component. As you can observe, the database operation, specifically the vector search operation took a fraction of a millisecond. And now the recommendation of the system is very different, as this recommended, a Sydney Hyde Park city apartment. This is different to the recommendation from the previous process earlier. Pause the video here and observe the results returned. One thing you will notice is, the return results meet the specified filters, which include the limits of the accommodates and the limit imposed on the bedrooms. Just to compare the two methods, which is the post filtering and pre-filtering. With the pre-filter, you will observe that the database operation returned to us 20 records. This is because we pre-filter the documents and the vector search operation returned the number of documents we specified. This is where you specified to return 20 documents. You will also notice a difference in the post-filtering, you specified 20 documents in the vector search operation, but only four was returned. This is your post-filtering results where you got just four documents from the database operation. This is because you conducted the vector search operation and then placed a match stage, which filtered the documents, returned to a lower number matching your condition. All right. In this lesson, you implemented a RAG pipeline with vector search that has a post-filtering step. Then you added a vector search operation with a pre-filtering step. And you observed the difference. In the next lesson, you're going to see how to reduce the amount of data returned from the database operation. See you there.