Skip to content

CS-DaviMagalhaes/Multimodal_Database

Repository files navigation

Multimodal Database

Sistema de Base de Datos Multimodal con Indexación Avanzada

Índice


Parte 1

Backend (FastAPI)

Este backend simula un sistema de base de datos que interpreta consultas SQL básicas (CREATE TABLE, INSERT, SELECT, CREATE INDEX) y las ejecuta sobre archivos binarios. Usa una estructura de almacenamiento personalizada junto con algoritmos de índices como B+ Tree, con soporte planificado para AVL y secuencial.

Correr Backend

Primero es necesario extraer los descriptores (o cargar los archivos si es que ya fueron generados). Para eso es necesario correr los notebooks en Image_descriptors, audio_descriptors y text_descriptors. Note que en el notebook de Image_descriptors es necesario cambiar la ruta del dataset de imagenes.

Luego correr backend con python backend/app.py.

Funcionalidades implementadas:

  • CREATE TABLE:

    • Guarda la estructura de la tabla en un archivo .meta dentro de /tablas/.
    • Crea un archivo .tbl binario para almacenar los registros.
  • INSERT INTO:

    • Inserta datos en el archivo binario correspondiente.
    • Valida claves primarias si están definidas.
    • Si existen índices para las columnas, los actualiza automáticamente.
  • CREATE INDEX:

    • Soporte actual para índice BPLUS, pronto para AVL y SEQUENTIAL.
    • Crea un archivo .idx en /indices/ con estructura binaria.
    • Permite usar estructuras de árbol para búsquedas rápidas.
  • SELECT:

    • Soporta:
      • SELECT * FROM tabla
      • SELECT columna1, columna2 FROM tabla
      • SELECT ... WHERE columna = valor
      • SELECT ... WHERE columna BETWEEN valor1 AND valor2
    • Si hay índice creado en la columna usada en WHERE, se utiliza automáticamente para optimizar el acceso (usa search o rangeSearch).
    • Si no hay índice, la búsqueda es secuencial.

📂 Organización de archivos:

Carpeta Contenido
tablas/ .meta y .tbl por cada tabla
indices/ Archivos .idx por índice creado
algoritmos/ Implementaciones de índices

🔐 Estructuras de archivo:

  • Header del archivo .idx: contiene info sobre posición root, libres, eliminados.
  • Registro binario .tbl: estructurado con struct.pack según tipos definidos en .meta.

Frontend (React)

Este frontend es una interfaz web simple para interactuar con el backend simulando una consola SQL. Permite enviar consultas manuales y visualizar resultados en tiempo real.

Correr Frontend

Entrar al directorio frontend y correr los siguientes comandos:

  • npm install
  • npm start Ignorar los warnings. Luego es solo entrar al enlace de localhost.

✨ Características:

  • Textarea para consultas SQL:

    • Envía la consulta como JSON al backend (POST /query)
    • Compatible con todos los comandos mencionados
  • Autocompletado básico:

    • Sugiere palabras clave SQL (SELECT, WHERE, etc.)
    • También sugiere nombres de tablas y columnas conocidas
    • Inserta automáticamente la palabra seleccionada
  • Visualización de resultados:

    • Si el resultado es un SELECT, se renderiza una tabla HTML con los datos.
    • El resultado crudo (JSON) también se muestra debajo como referencia técnica.
  • Botón adicional "Ver registros":

    • Llama directamente al endpoint GET /select/{tabla} para obtener todos los registros (búsqueda secuencial).

🧩 Estado del frontend:

  • Interfaz responsiva y fácil de extender
  • Backend y frontend conectados por defecto en http://localhost:3000http://localhost:8000

Dataset

Utilizamos el dataset cities que tiene 148061 registros con los siguientes atributos:

  • id: id de la ciudad
  • name: nombre de la ciudad
  • state_id: id del estado
  • state_code: código del estado
  • state_name: nombre del estado
  • country_id: id del país
  • country_code: código del país
  • country_name: nombre del país
  • latitude: coordenada latitud
  • longitude: coordenada longitud
  • wikiDataId: id de la ciudad registrado en wikidata.org

Extendible Hashing

Extendible Hashing que mantiene el índice en RAM y los buckets en disco, permitiendo inserciones, búsquedas, splits y overflows de forma eficiente.

Estrategias utilizadas en la implementación

Estructura general

Mientras estamos trabajando sobre el archivo del índice lo almacenamos en RAM para poder hacer operaciones de forma más eficiente. Cada cambio hecho al índice en RAM también se hace al archivo del índice, así podemos tenerlo en disco y abrirlo nuevamente después. Por ejemplo el índice para 1 millón de datos con 32768 entradas pesa menos de 1MB.

Se tiene un factor de balanceo fb que indica la cantidad de registros por bucket y una profundidad global D para poder manejar los splits.

Los buckets se construyen sobre el mismo archivo data.bin. Para cada bucket tenemos un header que almacena lo siguiente:

  • El número del bucket en decimal bucket_id
  • La profundidad local d
  • El siguiente bucket de overflow next
  • La cantidad de registros no vacíos en el bucket size
  • fb registros que se inicializan como registros en blanco

Reservamos fb registros para cada bucket. Simplemente esos registros son el constructor por defecto Registro(), representando un registro vacío. Cada vez que se inserta sobreescribimos esos registros y actualizamos el size del bucket, la cantidad de registros no vacíos.

Search

Se implementó la función get_reg_attributes() que la búsqueda de un elemento en los buckets principales y overflow. Retorna el registro encontrado en la búsqueda (si existe), la posición del registro y el número del bucket. Así podemos simplemente reutilizar esta función en search() y remove(). El registro encontrado lo usamos en la búsqueda y los otros dos valores del retorno usamos en el método de borrado para poder acceder directamente al registro y bucket asociado. Así evitamos repetir código.

Remove y reconstrucción

La estratégia de borrado que utilizamos es la siguiente:

  • Simplemente reemplazamos el registro que queremos borrar por un "registro vacío" (constructor por defecto de Registro()).
  • Contamos la cantidad de buckets vacíos (sin registros reales).
  • Si la cantidad de buckets vacíos sobrepasa a los 40% hacemos una reconstrucción total del índice y de los buckets.

Insert

Aplicamos hash al id numerico de los registros. Simplemente se inserta en el primer espacio vacío que se encuentra del bucket correspondiente a ese hash. Así reutilizamos los espacios que fueron borrados en remove. Si la profundidad local del bucket es menor a la profundidad global hacemos split en caso el bucket esté lleno. En caso ya no se pueda hacer split creamos buckets de overflow y los encadenamos.

Experimentación y Resultados

Para la experimentación variamos los parámetros fb y D para poder tener una distribución de registros y un índice balanceado, ya que las pruebas varían en la cantidad de datos. Así logramos tener tiempos eficientes para inserción, búsqueda y borrado. Utilizamos los siguientes valores:

Registros fb D
1k 4 8
10k 6 10
100k 6 10
250k 10 12
500k 16 14
1M 18 15

Con estos parámetros obtuvimos la siguiente cantidad de buckets y entradas en el índice: Cantidad de buckets vs entradas índice

Insert

Hicimos la inserción de acuerdo a los valores de fb y D en la tabla de arriba:

Insert

Search

Hicimos la búsqueda de 100 keys aleatorios del dataset y sacamos el promedio:

Insert

Remove

Hicimos el borrado de 100 keys aleatorios del dataset y sacamos el promedio:

Remove


ISAM

La implementación del sistema de acceso secuencial indexado (ISAM) se diseñó con el objetivo de simular un sistema de archivos jerárquico eficiente, con soporte para inserción, búsqueda y eliminación. A continuación se detallan las estrategias clave empleadas:

1. Estructura Multinivel

Se utilizó una estructura jerárquica de dos niveles de indexación:

  • Root Index (root_index.bin): Apunta a páginas del índice intermedio.
  • Index Pages (index_pages.bin): Contienen referencias a páginas de datos.
  • Data Pages (data_pages.bin): Almacenan los registros ordenados de forma secuencial.

Esto permite búsquedas logarítmicas y navegación eficiente por los niveles del índice.

2. Segmentación por Archivos

Cada nivel de la estructura se almacena en un archivo binario independiente:

  • root_index.bin
  • index_pages.bin
  • data_pages.bin
  • overflow.bin (para las inserciones que no se pudieron hacer en el área principal)

Esta segmentación facilita el manejo modular del almacenamiento y el acceso por niveles.

3. Bloques con Factor Fijo

Se utilizó un factor de bloqueo fijo:

  • BLOCK_FACTOR_DATA = 5: Número máximo de registros por página de datos.
  • BLOCK_FACTOR_INDEX = 4: Número máximo de entradas por página de índice.

Esto asegura una estructura uniforme de páginas, facilita el cálculo de offsets y permite navegación por saltos fijos.

4. Registros Fijos y Serialización con struct

Todos los registros (Registro) e índices (IndexEntry) se serializan usando el módulo struct, con un formato fijo (FORMAT) que asegura un tamaño constante en disco. Esto permite:

  • Cálculo directo de posiciones (offsets) sin recorrer todo el archivo.
  • Acceso eficiente a cualquier página o registro.

5. Área de Desborde (overflow.bin)

Las inserciones no se hacen en las páginas de datos (por la naturaleza estática del ISAM), sino que se redirigen a un archivo de desborde. Esto mantiene el área principal inalterada y permite realizar operaciones de inserción sin reorganizar el índice.

6. Búsqueda Binaria Aproximada por Niveles

Para cada búsqueda:

  1. Se localiza la página adecuada en root_index.bin.
  2. Luego se baja a la página de índice correspondiente.
  3. Finalmente se accede a la página de datos que contiene o debería contener el registro.
  4. Si no se encuentra, se busca en el overflow.bin.

Esta estrategia imita el comportamiento real del ISAM y permite realizar búsquedas eficientes con costo O(log n) en el mejor caso.

7. Lectura y Escritura Página por Página

Para mejorar la eficiencia y evitar acceso por registro, se leen y escriben bloques completos (páginas) en lugar de registros individuales.


Sequential File

La implementación del Sequential File se diseñó para que trabaje en un solo archivo (sin metadata) con cabezera, usando las técnicas del espacio auxiliar.

Espacio auxiliar

El archivo esta dividido en dos partes: la página principal y el espacio auxiliar. En este último es donde se irán insertando los registros hasta llegar a un threshold ($O(n)$ donde $n$ es la cantidad de registros en la página principal).

Una vez alcanzado, reconstruiremos la página principal, donde se reorganizarán todos los registros (incluyendo los del espacio auxiliar) ordenados por una key predeterminada por el usuario.

El propósito de este diseño es para resolver el problema de desborde de espacio, así como ahorrarse las complejidades de mantener ordenados todos los registros constantemente.

1. Inserción

En nuestra implementación, la inserción ocurre dentro del espacio auxiliar, donde se apilan los registros. Con cada inserción actualizamos el conteo de registros en la cabezera. Si dicha inserción activa una alerta de desborde, entonces se reconstruye el archivo, limpiando el espacio auxiliar.

Descontando el coste de reconstrucción, las inserciones cuenta con complejidad $O(1)$.

2. Búsqueda singular

El diseño de la implementación le otorga al sequential file la capacidad de búsquedas binarias bajo una key. Este tipo de búsquedas se caracterizan por ser el tipo de búsqueda más eficiente, puesto que cuenta con complejidad de apenas $O(\log n)$.

En nuestra implementación, partimos de una búsqueda binaria a la key, lo que devuelvue la primera aparición del registro con la key objetivo. En caso existan otros registros con la misma key, se iteran sobre los registros siguientes hasta que encuentre un key diferente

3. Búsqueda por rango

Para las búsquedas por rango, se aplicaron dos búsquedas binarias para ubicar las posiciones del elemento iniclal y final del rango. Una vez encontrado, solo se recorría dicho rango se forma linear.

Este diseño nos da una complejidad $O(\log n)$ para las búqueda de los índices y $O(m)$ para recorrer el rango. Asumiendo que $m < n \to m \approx \log n$, nos da una complejidad final de $O(\log n)$.

4.Borrado

Para el borrado, solo basta usar la misma lógica del search(), solo que en vez de almacenar los matches, activamos la flag deleted del registro. El espacio liberado por estos será utilizado en el próximo rebuild().

Experimentación y resultados

Para la fase de experimentación de este índice, se utilizó un slice del dataset cities de distintos tamaños $(1000, 5000, 1k, 15k, 20k, 25k, 30k)$ y se determinó como llave al id de los registros. Se analizaron los tiempos de ejecución de cada uno de los métodos para cada tamaño del dataset, con el fin de analizar su rendimiento.

Inserción

insert_seq

Search

Se aplicó un search() a 100 elementos aleatorios, promediando el tiempo final.

search_seq

Range Search (rango de tamaño 100)

range_seq

Borrado

Se aplicó un remove() a 100 elementos aleatorios, promediando el tiempo final.

remove_seq


RTree

Para la implementación del índice RTree, se diseñó para desacoplarlo del archivo principal, siendo este índice almacenado en múltiples archivos metadata, y se utilizó principalmente la librería rtree de Python.

El archivo principal solo se encargaría de apilar los registros, siendo una interfaz para el índice RTree. Para manejar los borrados, cuenta con una free list almacenada en otro archivo aparte.

Librería rtree

Esta librería aportó con la implementación de la estructura de datos, así como sus métodos para las inserciones, queries y borrados. Cuenta con métodos para almacenarse en memoria secundaria a través de archivos .rtree.dat y .rtree.idx. Sin embargo, esta librería contaba con limitaciones que requerían el uso de un archivo metadata personalizado para este proyecto:

  • No maneja puntos, por lo que cada registro fue insertado como un rectángulo de area 0.
  • La llave de cada una de las hojas requerían un orden incremental, por lo que se utilizó la posición de cada registro en el archivo principal en lugar de su id.
  • No cuenta con métodos para búsquedas por radio.

Metadata

Se creó un archivo Metadata que almacenaba los campos necesarios de cada registro para los métodos del RTree (como posición, longitud, latitud). Principalmente usado por la búsqueda radial y el borrado.

Métodos

Gracias a la librería, la implementación de los métodos del RTree consistió en convertir los hiperparámetros en los adecuados para ejecutar el correspondiente del rtree. Solo dos métodos necesitaron de lógica extra para su correcto funcionamiento:

  • radius_search((x, y), r): se realizó una query sobre el rectángulo definido por los puntos $(x-r, y-r)$ y $(x+r, y+r)$. Una vez obtenidos los registros extraídos, se filtraron aquellos que excedían su distancia hacia el centro $(x, y)$ calculado con la distancia euclidiana.
  • erase(pos): usando la metadata, se obtenían las coordenadas lon y lat del registro, puesto que rtree no elimina elementos definidos por la key, sino que tambíen necesita de sus coordenadas.

Tanto la inserción como la búsqueda por rectángulo y la búsqueda KNN fueron interfaces de los métodos insert(rectangle), intersects(rectangle) y nearest(rectangle, k) del rtree, siendo los puntos pasados como rectángulos de área 0.

Experimentación y resultados

Para la fase de experimentación de este índice, se utilizó un slice del dataset cities de distintos tamaños $(1000, 5000, 1k, 15k, 20k, 25k, 30k)$. Se analizaron los tiempos de ejecución de cada uno de los métodos para cada tamaño del dataset, con el fin de analizar su rendimiento.

Inserción

insert_rtree

Box Query

box_rtree

Radial Query

radial_rtree

100-Nearest Neighbors

knn_rtree

Borrado

Se aplicó un remove() a 100 elementos aleatorios, promediando el tiempo final.

remove_rtree


B+ Index

Con el objetivo de mejorar la eficiencia en las búsquedas sobre archivos de datos, se implementó un índice basado en un árbol B+ no agrupado (unclustered). Esta estructura permite mantener las claves ordenadas y enlazadas en nodos hoja, mientras que los datos reales se almacenan en un archivo separado. Las hojas contienen punteros a la posición física del registro en el archivo de datos.

Además, cada nodo hoja mantiene un campo next que enlaza con la siguiente hoja, lo que permite recorridos eficientes y búsquedas por rango. El árbol maneja claves de tipo int o string (hasta 30 caracteres), según el parámetro de configuración.

La estructura está respaldada por un archivo binario que almacena los nodos, con una cabecera que contiene:

Posición del nodo raíz

Posición libre (para insertar nuevos nodos)

Posición del nodo eliminado (para reutilización de espacio)

1. Inserción

La inserción sigue el recorrido típico del árbol B+, localizando la hoja adecuada para insertar la nueva clave. Si la hoja tiene espacio, se inserta ordenadamente. En caso de desbordamiento, se realiza una división (split) del nodo y se propaga hacia arriba si es necesario, manteniendo la altura mínima del árbol.

Cada clave insertada en una hoja se enlaza con una posición exacta en el archivo de datos, actuando como índice secundario.

La complejidad de inserción es $O(\log n)$, correspondiente a la altura del árbol.

2. Búsqueda singular

Para realizar búsquedas puntuales, el índice recorre desde la raíz hasta una hoja aplicando comparación binaria en cada nodo para determinar el hijo correspondiente. Una vez en la hoja, se verifica si la clave existe y se retorna la posición del registro en el archivo de datos.

Este diseño garantiza una complejidad de búsqueda $O(\log n)$.

3. Búsqueda por rango

Gracias a que las hojas están enlazadas mediante el campo next, la búsqueda por rango es eficiente. Primero, se localiza la primera clave del rango mediante búsqueda estándar. Luego, se recorren las hojas sucesivas mientras las claves estén dentro del rango.

Este diseño ofrece complejidad $O(\log n)$ para ubicar el inicio del rango y $O(m)$ para recorrer $m$ claves dentro del mismo. Bajo el supuesto de que $m \approx \log n$, la complejidad se mantiene en $O(\log n)$.

4. Borrado

Actualmente, el índice no implementa una operación de borrado. Sin embargo, se ha considerado la gestión del espacio libre mediante un campo de posición eliminada en la cabecera, pensado para futuras extensiones que permitan reutilizar nodos liberados.

Experimentación y resultados

Para evaluar el rendimiento de nuestro índice B+, se realizaron pruebas de inserción sobre un subconjunto del dataset cities, usando distintos valores del orden del árbol m (5, 10 , 25 y 100). Cada configuración fue evaluada registrando el tiempo total de inserción y el tamaño final del archivo generado.

Los resultados fueron los siguientes:

m = 5: Tiempo de inserción = 36 segundos, Tamaño del archivo = 3.9 MB

m = 10: Tiempo de inserción = 33 segundos, Tamaño del archivo = 3.3 MB

m = 25: Tiempo de inserción = 31 segundos, Tamaño del archivo = 2.8 MB

m = 100: Tiempo de inserción = 28 segundos, Tamaño del archivo = 2.6 MB

Como se puede observar, aumentar el valor de m mejora tanto el tiempo de inserción como el uso de espacio, debido a que se reducen las divisiones de nodos y se mejora la compactación del árbol. Esto confirma que una mayor capacidad de fan-out en los nodos del B+ Tree puede resultar beneficiosa para datasets de tamaño considerable.


Comparación entre estructuras

Para cerrar la parte 1, se realizaron nuevos tests para comparar el rendimiento de los 5 índices implementados sobre las operaciones de inserción, queries y borrado. Cabe aclarar que dependendiendo de la implementación, ciertos índices no contaban con el soporte de ciertas operaciones. Además, el RTree solo soporta queries espaciales, por lo que su análisis irá separado al resto.

  • El tamaño de datasets para esta comparación fueron 1K, 10K, 50K y 100K registros del dataset cities.csv.
  • Extendible Hashing: se usó un factor de balanceo fb = 6, profundidad máxima D = 10 y un threshold de buckets vacíos EMPTY_THRESHOLD = 0.4
  • B+ Tree: se usó un orden m = 10

Inserción

insertion_times_gauged

Búsqueda

Para las búsquedas, se usaron 100 keys aleatorias, y se obtuvo el tiempo promedio de búsqueda.

search_times_gauged

Búsqueda por rango

range_search_times_gauged

Borrado

Para el borrado, se usaron 100 keys aleatorias, y se obtuvo el tiempo promedio de borrado.

remove_times_gauged

ISAM: Construcción

Como alternativa a las inserciones, se midió el tiempo de construcción del índice ISAM usando el método ISAM.build().

build_times_gauged

RTree: Spatial Queries

rtree_times_gauged


Parte 2

Text Search

Implementamos la busqueda de textos por similitud

Dataset

Utilizamos el dataset Legal Citation Text Classification que consiste en 24985 casos legales que cuentan con el resultado final, el titulo del caso y de que trataba.

Estrategias Utilizadas

Construimos un sistema de recuperación de información textual a partir de un dataset con columnas: case_id, case_outcome, case_title, y case_text. Los textos fueron preprocesados y luego indexados utilizando un enfoque optimizado del algoritmo SPIMI (Single-Pass In-Memory Indexing) para permitir escalabilidad y eficiencia en almacenamiento y consulta.

Preprocesamiento

Se combinaron los campos textuales por fila y se aplicó la siguiente pipeline:

  • Tokenización de palabras
  • Eliminación de signos y símbolos no alfabéticos
  • Filtrado de stopwords utilizando el corpus de NLTK
  • Reducción mediante Stemming usando PorterStemmer

El resultado fue una lista normalizada de tokens para cada documento, lista para ser indexada.

Construcción del Índice Invertido

Se utilizó una versión híbrida de SPIMI adaptada para escritura a disco por bloques, con el fin de evitar saturar la memoria RAM incluso en grandes colecciones de datos.

  • Partición por bloques: se procesaron documentos en bloques de 10,000 filas (block_size=10000).
  • Postings list por término: para cada término en cada bloque, se almacenó la lista de (doc_id, tf_weight).

Cada bloque se guardó como un archivo .pkl independiente con la estructura:

{ term1: [(doc_id, tf1), ...], term2: [...], ... }

También se guardaron:

  • La norma precomputada de cada documento en norms.pkl.
  • La frecuencia de documentos (df) por término en df_counter.pkl.

tal que obtenemos la siguiente estructura en disco:

index_blocks/
├── block_0.pkl
├── block_1.pkl
├── ...
├── norms.pkl
├── df_counter.pkl

Consulta (Top-k por Similitud de Coseno)

La búsqueda se diseñó para:

  • Evitar cargar el índice completo en RAM.
  • Acceder solo a los bloques que contienen postings de los términos consultados.

Para cada término t de la consulta:

  1. Se calcula el peso TF-IDF del término en la query.

  2. Se itera sobre todos los bloques block_i.pkl:

    • Se carga el bloque.

    • Si el término t está presente, se actualizan los scores de los documentos usando:

      score(q, d) = sum(w_tq * w_td for t in query_terms) / (norm_q * norm_d)
      
  3. Se normaliza con la norma de la consulta y la norma del documento precalculada.

  4. Se utiliza heapq para mantener solo el Top-k documentos más similares.

Experimentación

Se realizaron pruebas con diferentes tamaños de bloque (block_size) y queries variadas para observar el impacto en tiempo y uso de memoria. Utilizamos un tamaño de bloque de 10000 y realizamos consultas para comparar los resultados con el modelo tsvector y gin de Pgadmin4

Comparación de Resultados: PostgreSQL vs SPIMI

Query Rank PostgreSQL (case_id) PostgreSQL (Score) SPIMI (case_id) SPIMI (Score)
contract 1 Case22026 0.2569325 Case22871 0.2982
breach 2 Case16659 0.2491805 Case24032 0.2837
3 Case24791 0.2438541 Case24032 0.2837
4 Case24786 0.2438541 Case24027 0.2794
5 Case24806 0.2438541 Case11880 0.2768
high 1 Case24280 0.0016323 Case9 0.2427
court 2 Case9 0.0016129 Case24281 0.2001
defamation 3 Case24283 0.0013625 Case540 0.1961
plaintiff 4 Case540 0.0010989 Case541 0.1961
5 Case541 0.0010989 Case24295 0.1832

Image Search

Implementamos la búsqueda de imagenes por similitud, con indexación de descriptores locales.

Dataset

Utilizamos el dataset Fashion Products Small que consiste de 45k fotos de articulos de moda (ropa, accesorios, etc). Este dataset es la versión más ligera del dataset Fashion Product Images Dataset. Las imagenes son de menor resolución, para ahorrar espacio y obtener mayor rapidez en la extracción de features.

Estrategias Utilizadas

Utilizamos SIFT para la extracción de características, el cual extrae un vector de descriptores que tiene 128 dimensiones. Utilizamos PCA para reducir la dimensionalidad a 70 dimensiones, capturando el 95% de la varianza de los datos. Hicimos un análisis de la cantidad de componentes y las varianzas acumuladas:

pca analysis

Luego aplicamos KMeans para agrupar los descriptores en clusters. Intentamos utilizar los labels que contiene la columna articleType en el archivo styles.csv para poder realizar un análisis de precisión variando la cantidad de clusters para KMeans entre 10 y 500 clusters. Sin embargo este análisis resulto inefectivo ya que no consideró completamente la similitud visual de los items. Salieron mejores precisiones con 400+ clusters pero visualmente no era así, ya que obtuvimos más similitud visual con la query utilizando menos clusters. Es por ello que hicimos un análisis visual para elegir la cantidad de clusters:

Kmeans Cluster Analysis

El siguiente paso fue construir histogramas para luego aplicar TF-IDF como técnica de ponderación para cada visual word. En base a eso pudimos aplicar KNN secuencial y KNN con indexación invertida para obtener las Top-k imagenes más similares a una query en base a la similitud coseno.

Utilizamos heapq para las funciones de KNN para mayor eficiencia, de modo que siempre mantenemos los Top-k resultados más similares en el heap y hacemos pop al menos similar en cada iteración. Es por eso que no pusheamos valores negativos al heap.

Experimentación

Utilizando K = 8 y 5 ejecuciones para poder calcular el tiempo promedio. Tiempos en segundos.

N KNN Secuencial KNN Indexado KNN PostgreSQL
1000 0.0446 0.0009 0.0018
2000 0.0981 0.0018 0.0022
4000 0.2029 0.004 0.0037
8000 0.3573 0.0099 0.0058
16000 0.9109 0.0186 0.084
32000 1.538 0.0418 0.0902
64000 2.9061 0.0875 0.1231

Tiempos knn imagenes


Music Search

Implementación de búsqueda de canciones por similitud.

Dataset

Utilizamos el dataset de Music Bench que contiene fragmentos de canciones y grabaciones de música de aproximadamente 10 segundos. Específicamente, usamos la versión ligera FMACaps que contiene 1000 archivos .wav.

Estrategias Utilizadas

Utilizamos MFCC (Mel-Frequency Cepstral Coefficients) para la extracción de descriptores de audio, que comprime frecuencias altas y da más resolución a las bajas, procesando el audio por ventanas de tiempo.

Las demás funciones utilizadas son prácticamente las mismas que utilizamos para image search. Solo que aqui ya no utilizamos PCA y también cambiamos el número de clusters para KMeans.

Experimentación

Utilizando K = 8 y 5 ejecuciones para poder calcular el tiempo de ejecución promedio. Tiempos en segundos.

N KNN Secuencial KNN Indexado KNN PostgreSQL
1000 0.0356 0.0012 0.0023
2000 0.0631 0.002 0.0017
4000 0.1446 0.0037 0.0028
8000 0.275 0.007 0.0047
16000 0.49 0.0146 0.0068
32000 0.9849 0.03 0.1269
64000 2.0744 0.0767 0.1293

Knn comparison audio


About

Sistema de Base de Datos Multimodal con Indexación Avanzada

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •