I want to build an autocomplete form in a Django webapp. I have already been able to make the search bar in which I query my MongoDB database but how can I add an autocomplete? I have 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 class="form" action="{% url 'similar_results' %}" method="get">
<input name="q" type="text" placeholder="Perfume name...">
<input id="perfumename" type ="submit" value="Find Similar Perfumes"/>
</form>
</div>
<script>
$(document).ready(function() {
$("#perfumename").autocomplete({
source: async function(request, response){
let data=await fetch(`http://localhost:8000/similar/similar_results?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 though I have autocomplete="nope" the first search bar still shows chrome's default autocomplete and doesn't show the one I built in MongoDB.
I wonder if it's not a problem in the javascript, but I'm bad at javascript. Indeed the url where we arrived when we did when we press the button:
urls.py
path('similar/similar_results/', views.SearchResultsView.as_view(), name='similar_results'),
views.SearchResultsView
:
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([x["name"] for x in object_list])
return [x["name"] for x in object_list]
And it returns everything I need:
['Not a Perfume - Coffret Noël Not a Perfume', 'Festival Nite Pour Lui – Coffret Noël Eau de Toilette', ... , 'Parfum de Peau - Huile Parfumée Roll-on'
In my opinion it is possible that there is the problem in the javascript: http://localhost:8000/similar/similar_results?q={request.term}
.
Update
I tried Mauricio Conteras's solution but I have problems in the backend (which I think I solve) and the frontend that I haven't solved.
back-end
I had problems with the backend. Actually I get the following error: pymongo.errors.OperationFailure: Remote error from mongot :: caused by :: autocomplete index field definition not present at path XXX
. With the following pipeline:
pipeline = [
{
"$search": {
"index": "default", # nombre de mi index
"autocomplete": {
"path": "name", #el nombre del field sobre cual hizo el index
"query": str(query) # query = self.request.GET.get('q')
}
}
},
{
"$limit": 10
},
{
"$project": {
"_id": 0,
"title": 1
}
}
]
However, I put nombre
as the path of the field definition of the index:
But with the original I return what I need:
pipeline = [
{
'$search': {
'index': 'default',
'compound': {
'must': {
'text': {
'query': str(query),
'path': 'name',
'fuzzy': {
'maxEdits': 2
}
}
}
}
}
},
{
"$limit": 10
},
{
"$project": {
"_id": 0,
"name": 1
}
}
]
result = list(collection.aggregate(pipeline))
data = {'data': result}
print(data)
return JsonResponse(data)
Bring back:
{'data': [{'name': 'Not a Perfume - Coffret Noël Not a Perfume'}, {'name': 'Festival Nite Pour Lui – Coffret Noël Eau de Toilette'}, {'name': 'Festival Nite Pour Ell
e – Coffret Noël Eau de Parfum'}, {'name': "For A Kiss 'Iconic Love' - Coffret Noël Eau de Toilette"}, {'name': 'Daisy Love - Eau de Toilette'}, {'name': 'Crystal No
ir - Eau de Parfum'}, {'name': 'Rose Pompon - Eau de Toilette'}, {'name': 'Candy love - Eau de Toilette'}, {'name': 'Drakkar Noir - Eau de Toilette'}, {'name': 'Quat
re en Rose - Eau de parfum'}]}
front-end
I have tried to display the data retrieved from the backend in a simple alert:
function autocomplete(value) {
if(!value) return;
currentFocus = -1;
url = `/similar/similar_results/?query=${value}`;
fetch(url)
.then(res => res.json())
.then(json => {
let data= json.data;
alert(data);}) // <-- Aqui intento muestrar los datos que provienen del backend
.catch(e => {
console.error(e.message);
console.log('Uuuuups');
});
}
But I never get this message.
In the console I get:
(index):99 Unexpected token < in JSON at position 2
(anonymous) @ (index):99
Promise.catch (async)
autocomplete @ (index):98
(anonymous) @ (index):140
(index):100 Uuuuups
Which seems to come from the try catch in the javascript:
Mea maxima culpa
What is returned is an html page. I think I use as endpoint what is used to return the results page when you did Enter
ISSUE
You want to make an autocomplete list in html , so that it contains the values according to a query to a MongoDB Atlas database , performed using an index
autocomplete
in an aggregation process using the operator$search
(exclusive to Mongo Atlas).SOLUTION
A possible solution is to use a call
fetch
or callajax
to our backend, in such a way that we are returned a list of elements that match what is typed in the input element (input
) of the search form.The idea is to make a request to the Database using the value of the element as the lookup value
input
, and generate a suggestion list (auto-complete) that matches what has been written.Assuming you have a decent connection speed, you can accomplish the task without much trouble.
In this example we will use pure Javascript, with no third party libraries, except for certain CSS3 global styles, which we will apply using Bootstrap .
back-end
Primero vamos a configurar el
endpoint
de nuestro backend, escrito en Django. No tocaremos la configuración de índices de MongoDB Atlas, ya que se asume que los mismos ya han sido configurados para búsqueda de autocompletado.Supongamos que usaremos el siguiente
endpoint
que recibe un valor mediante una Query URL:En este endpoint estamos enviando un par
<clave>=<valor>
en laurl
de la solicitud.Así, podemos crear la siguiente función en el archivo
views.py
de nuestra aplicación:Este es un ejemplo de búsqueda de autocompletado en la base de datos
my_database
usando el índice llamadorevolucion
. Vemos que elpipeline
de agregación para la etapa$search
es de tipoautocomplete
, sobre el campotitle
y usando como elemento de consulta (query
) el valor obtenido de laurl
de la petición. En este ejemplo, se están limitando los resultados a sólo 10, pero puedes ajustar el valor a un número más alto o puedes eliminar totalmente la etapa$limit
para que devuelva todos los resultados que coincidan con la consulta.El resultado de dicha consulta a MongoAtlas se almacena en un diccionario dentro de la clave
data
y luego se envía este diccionario en una respuesta tipojson
al cliente que realizó la solicitud.Esto es lo básico de un
endpoint
en Django para realizar la consulta de autocompletado a la Base de Datos y enviar el resultado al cliente.Frontend
Ahora, la parte más complicada está en generar una lista de autocompletado que se actualice de acuerdo a lo que el usuario escribe en el elemento de tipo
input
. Existen diversas formas de lograr el objetivo, pero usaremos una forma básica que implica programación en javascript y conocimiento de html y css.Lo primero es trabajar en la lógica de petición a nuestro
backend
. Para ello usaremos la APIfetch
:Esta es la forma en que podemos hacer la petición a nuestro
endpoint
en Django. La lógica para escribir el resultado como una lista de sugerencias de autocompletado varía de acuerdo los resultados deseados.En este ejemplo usaré algunas funciones que se encargarán de dar formato tanto a la lista como al resultado obtenido. Además, usaré una llamada a un
endpoint
que será:api/movies
. En mi ejemplo usaré la base de datos de prueba que provee Mongo Atlas.La primera función que vamos a implementar se llamará
autocomplete
, y será como se ve a continuación:Analicemos esta función en detalle, ya que es la que realiza todo el proceso de generar la lista de autocompletado.
Se recibe un valor, y se establece una variable (que usaremos más adelante), llamada
currentFocus
a -1. Se crea una cadenaurl
dinámicamente usando el valor recibido. Se hace la llamada a nuestroendpoint
usando laurl
generada.El resultado de dicha llamada se convierte en un objeto de Javascript usando el método
json()
del objetoResponse
recibido. Una vez que tenemos nuestro objeto, llamamos a una función llamadacloseAllLists()
(que veremos más adelante) y procedemos a obtener los datos recibidos.Se crea un elemento de tipo
div
, y le añadimos un identificador al mismo (atributoid
), también le añadimos una clasecss
para darle el aspecto que definiremos con los estilos.Seleccionamos el elemento que contendrá nuestra lista de resultados y le agregamos el
div
que acabamos de crear, como nodo hijo usandoappendChild()
.Por último vamos a recorrer cada elemento de la lista de datos devuelta, y por cada valor (en este ejemplo estamos buscando auto completar el título de una película), vamos a crear un nuevo elemento
div
que contendrá el valor devuelto por la BD. Además, en cada iteración, añadimos un manejador para el eventoclick
, para que al hacer clic sobre dicho elemento, elinput
tome ese valor. Al final, en cada iteración, añadimos el elementodiv
al contenedor de elementos.Ahora, lo que haremos es darle algo de diseño y por supuesto, acciones para el uso del teclado (usar teclas de dirección arriba y abajo y presionar Enter sobre un elemento seleccionado).
Esta lógica se basa casi toda en este ejemplo de
w3schools
. Si bien he cambiado algunos detalles, es básicamente el mismo concepto.La función
closeAllLists()
se encarga de eliminar del nodo contenedor principal los contenedores añadidos, para despejar o limpiar el camino para el siguiente grupo de auto completado de acuerdo a lo que se va escribiendo en el elementoinput
:La siguiente función se encarga de manejar el evento
keydown
cuando estamos escribiendo en el elementoinput
y deseamos seleccionar algún elemento de la lista que se despliega:Las siguientes 2 funciones se encargan de establecer los estilos o retirarlos según un elemento se encuentre seleccionado (
currentFocus
):Por último, debemos manejar los eventos
input
ykeydown
sobre nuestro elemento de texto, para que se ejecuten las acciones necesarias y se realice la búsqueda a BD del texto para el auto completado, además se añade un manejador para el evento "click" sobre cualquier sitio del documento diferente alinput
, para que el panel de auto completado se oculte.La implementación casi completa del HTML con los estilos y el script se puede apreciar en el siguiente código:
El resultado de esta implementación se puede observar en la siguiente aplicación Django subida a Heroku.
Nota
Cabe destacar, que para que este código funcione correctamente, se debe establecer la propiedad
autocomplete
del formulario aoff
:De lo contrario, el navegador llenará con los datos del historial de auto completado local, su propia lista de auto completado.
Espero que esto te ayude a resolver el problema.
The easiest way I could find is to use the jQuery API , you just need to import the jQuery min and the jQuery UI CSS/JS.
Here is an example:
You can change the API CSS theme to one of the following:
Reference where this lisa was taken from
You can also create a custom style based on one of the existing themes, one way could be to create your own CSS file and copy the content of one of these links, to modify it successively.
Estás recibiendo error al interpretar a JSON porque las propiedades y valores están encerradas entre comillas simples y deberían estar entre comillas dobles. Referencia: https://www.json.org/json-es.html
You must correct this from the backend . I don't know Python, but @MauricioContreras answer seems more than adequate, check the last line:
return JsonResponse(data)
and you have onlyprint(data)
. I guess you just have to include the corresponding library and print instead of returning.Whether you rely on Mauricio's answer or this tutorial , the ideal is to return a string containing valid JSON from the server.
Only as a last option, you can try it in frontend
Testing first with the data posted in your question:
Now, adapted to AJAX request, you must get the result as text in order to apply the changes and interpret as JSON: