Building an Autocomplete Form Element with Atlas Search and Django

Hi MongoDeBians!

I want to build an autocomplete form in a Django webapp. I have already been able to do the search bar where I query my MongoDB database but how can I add an autocomplete? I tried to adapt an official tutorial that does it with Javascript:

search_similar.html:

{% extends "todo/base.html" %}

{% block content %}

  <div class="recommendations">
    <!-- <div class="login-page"> -->
    <div class="form">
      <form action="{% url 'search_results' %}" method="get">
        <input name="q" type="text" placeholder="Perfume name...">
        <input id="perfumename" type ="submit" value="Find Similar Perfumes" autocomplete="nope"/>
      </form>
      <form class="login-form" action = "similar_results/" method="POST">
        <input type="text" placeholder="perfume name"/> <!-- https://www.youtube.com/watch?v=TRODv6UTXpM&ab_channel=ProgrammingKnowledge -->
        {% csrf_token %}
        <input id="perfumename2" type ="submit" value="Find Similar Perfumes" autocomplete="nope"/>
      </form>
    </div>
    <script>
    $(document).ready(function() {
        $("#perfumename").autocomplete({
          source: async function(request, response){
            let data=await fetch(`http://localhost:8000/search_similar?q={request.term}`)
            .then(results => results.json())
            .then(results => results.map(result => {
              return { label: result.name, value: result.name, id:result._id };
            }
            response(data);
          },
          minLength=2,
          select: function(event, ui){
            console.log(ui.item);
          }

        })
      }),
      $(document).ready(function() {
        $("#perfumename2").autocomplete({
          source: async function(request, response){
            let data=await fetch(`http://localhost:8000/search_similar?q={request.term}`)
            .then(results => results.json())
            .then(results => results.map(result => {
              return { label: result.name, value: result.name, id:result._id };
            }
            response(data);
          },
          minLength=2,
          select: function(event, ui){
            console.log(ui.item);
          }

        })
      })
    </script>
  </div>

{% endblock %}

Even when I have autocomplete="nope" the first search bar still shows up the default autocomplete by chrome and doesn’t show up the one I built in MongoDB. The second doesn’t show up anything, even when I link the id to the script.

enter image description here

Do you know how I can handle that?

Hi @Mike_MPC, my apologies for the late reply here, but I hope this message finds you well.

I re-ordered my response because one was long.

I noticed a couple things about your snippet and thought a few bits of information could be helpful.

The second detail I would point out is the minLength=2 line. I think that should be minLength: 2. It is a key value pair in the object that is the parameter/argument of the autocomplete function, along with source and select.

First, here is a repo containing a hacked together Flask app I forked to port its search functionality to Atlas Search. It’s not exactly Django, which adds some more constructs, but it’s similar so it could be helpful. There’s even an example index definition for autocomplete in the README. Be sure you have set that autocomplete index up in Atlas, otherwise it work work.

1 Like

Many thanks for your answer Marcus, no worries of being late. I was away for a few day as well.

Many thanks for your resource and for pointing out the error with minLength, I corrected it but it didn’t launch the autocomplete. So the error must be somewhere before. I’m reviewing my code and I will dive into your GitHub as long as I am sure the error isn’t from my side.

I am now reviewing my views.py which contains the query to MongoDB, I don’t know if it is realted to the autocomplete triggering. I guess not, but in such a case here it is:

class SearchResultsView(ListView):
    model = Perfume
    template_name = 'todo/search_similar_results.html'

    def get_queryset(self):
        query = self.request.GET.get('q')
        print("JE SUIS PASSE PAR LA")
        # object_list = list(collection.find({"q0.Results.0.Name": {"$regex": str(query), "$options": "i"}}))
        object_list = list(collection.aggregate(
            {
                "$search":{
                    "autocomplete":{
                        "query": query,
                        "path": "q0.Results.0.Name",
                        "fuzzy":{
                            "maxEdits":2,
                        }
                    }
                }
                # "q0.Results.0.Name": {"$regex": query, "$options": "i"}
            }
        ))
        print('type(object_list): ', type(object_list))
        return object_list

The findone query worked, without triggering the autocomplete, the aggregate doesn’t so I’m reviewing it at the moment. If it doesn’t work I will redisign that with your example.

Thanks again!

1 Like

Ahh, herein lies the issue @Mike_MPC. Unfortunately, autocomplete does not support positional paths (note: position 0 in the Results array). For this query to work, you would need to unwind the results array if possible. You have a few options in triggers and materialized views via $merge.

Let me know if that fixes the issue. You could test by simply changing the path to another field that is already a non-enumerable field.

1 Like

Thanks for that info @Marcus , I just created a non-enumarable field for all documents to deal with this positional path issue. I don’t have any error anymore but it doesn’t trigger the autocomplete either:

object_list = list(collection.aggregate([
            {
                "$search":{
                    "autocomplete":{
                        "query": str(query),
                        "path": "name",
                        "fuzzy":{
                            "maxEdits":2,
                        }
                    }
                }
                # "q0.Results.0.Name": {"$regex": query, "$options": "i"}
            }]  

So you’re saying that I need to unwind the results array if possible. However contrarily to my first attempt which was commented up above:

list(collection.find({"q0.Results.0.Name": {"$regex": str(query), "$options": "i"}}))

The result I get there is an empty array.

Okay, now the search query works:

class SearchResultsView(ListView):
    model = Perfume
    template_name = 'todo/search_similar_results.html'

    def get_queryset(self):  # new
        query = self.request.GET.get('q')
        print("JE SUIS PASSE PAR LA")
        # object_list = list(collection.find({"q0.Results.0.Name": {"$regex": str(query), "$options": "i"}}))
        object_list = list(collection.aggregate([
                {
                    '$search': {
                        'index': 'default',
                        'compound': {
                            'must': {
                                'text': {
                                    'query': str(query),
                                    'path': 'name',
                                    'fuzzy': {
                                        'maxEdits': 2
                                    }
                                }
                            }
                        }
                    }
                }
            ]
        ))
        print(object_list)
        return object_list

This get the results in function of what I have in the get query. However, it doesn’t trigger the autocomplete.

for autocomplete, you need to use the autocomplete operator instead of the text operator.

1 Like

Okay thanks @Marcus , that make sense. I guess I need to create a new function. But I’ve looked at your code and I didn’t understand how it was triggered.
Indeed, if getrestaurants() is triggered by the find-restaurants submit action button. I don’t know about suggest_restaurants().