• Featured post

Embeddings & Vector databases

Embeddings

Un ordenador no puede entender texto ni relación semántica o significado entre palabras. Solo puede entender números. Esto lo resolvemos mediante el uso de embeddings.

Un embedding es la representación de texto (en forma de números) en un espacio vectorial. Esto permite a los modelos de IA comparar y operar sobre el significado de las palabras.

flowchart TD
    A["perro"] --> B
    B --> C["[-0.003, 0.043, ..., -0.01]"]
    
    N1["(texto que queremos convertir)"]:::note --> A
    N2["(vectores con contenido semántico)"]:::note --> C
    
    classDef note fill:none,stroke:none,color:#777;    

Los vectores de cada palabra o documento capturan el significado semántico del texto.

  • perro estará cerca de mascota
  • contrato estará lejos de playa

Vector vs SQL databases

El problema con las BBDD típicas es que solo buscan matches exactos. Si yo busco por coche solo me sacará las entradas que contengan coche.

En cambio, como las BBDD vectoriales pueden interpretar la semántica de las palabras mediante los vectores, si busco por coche puede sacarme valores como sedán, SUV, Land Rover, etc.

Las BBDD vectoriales son muy buenas cuando necesitamos buscar items similares por proximidad uno respecto al otro. Un ejemplo de uso es buscar películas parecidas (Netflix). Otro ejemplo son los recomendadores de items parecidos en tiendas online (Amazon).

Como ejecutar una búsqueda (query) mediante vectores

(You can see the code here)

Necesitamos:

  • Una BBDD Vectorial (CosmosDB)
  • Un modelo para transformar los embeddings (text-embedding-3-large)

El flujo completo es el siguiente:

  1. Usar un embedding model para crear embeddings del contenido que queremos indexar
  2. Insertar el texto original y los vectores del contenido en una BBDD vectorial
  3. Cuando queramos ejecutar una query usar el mismo embedding model de antes con la query a buscar. Con el embedding resultante buscamos vectores similares en la BBDD y sacamos el texto original de original_text

    Introducir vectores en CosmosDB

    Para poder buscar necesitamos rellenar antes la BBDD con contenido. Lo mantenemos simple. Metemos

    • un ID a mano
    • el texto original
    • los vectores resultado de hacer el embedding sobre el texto original

El pseudocódigo se ve así y se ejecuta de uno en uno

def main():
	text = "A shiba walks alone in the park"
	# this sends the text to the model text-embedding-3-large 
	vectors = createEmbeddingsForText(text)
	item = {
		"id": "1",
		"original_text": text,
		"vectors": vectors
	}
	uploadToCosmosDB(item)

ejemplos de los datos que guardo

{
	"id": "1",
	"original_text": "A shiba walks alone in the park",
	"vectors": [-0.003, 0.043, ..., -0.001]
}

Read More

Entity Framework Core Scaffolding

Go to Tools > Nuget Package Manager > Package Manager Console

Adapt and paste the following code. This includes:

  • db’s connection string
  • name for the context it’s going to create
  • Where it pastes the data classes to
Scaffold-DBContext "Host=host_here;Database=database_name;Username=username_here;Password=pwd_here" Npgsql.EntityFrameworkCore.PostgreSQL -DataAnnotations -Context ContextNameHereDbContext -ContextDir Data -o Models/DB -force -NoOnConfiguring -verbose

This creates:

  • DbContext.cs class
  • all Model/Data classes

Remember to modify Startup.cs to set the service classes as AddTransient for the services which use this DbContext

services.AddTransient<SomethingService, SomethingService>(); 

Add into the service, the context you’ve just created and set it at the constructor.

private readonly ApplicationDbContext _context;

EFCore consultas solo lectura - gran volumen entidades

Cuando se van a realizar consultas de solo lectura, las cuales NO se van a usar para actualizar en la base de datos, se puede llamar al método .AsNoTracking() para mejorar la performance.
Es recomendable utilizarla para manejar grandes volumenes de entidades.

Las entidades de las queries donde se use .AsNoTracking() no se podrán actualizar, por lo que no es recomendable para ADD, UPDATE o DELETE

// operacion de solo lectura
var libros = context.Libros
	.AsNoTracking()
	.Where(l => l.Autor == "Something")
	.ToList();

Data Models Entity Framework Core

You can easily mark fields for a model which are primary key or are required.

public class User
{
	[Key]
	public int Id { get; set; }
	
	[Required(ErrorMessage = "Name is mandatory")]
	public string Name { get; set; }
	
	public string Telephone { get; set; }
	
	public string Mobile { get; set; }
	
	[Required(ErrorMessage = "Email is mandatory")]
	public string Email { get; set; }
}

Nullables C#

Tipos Nullable (?)

El operador ? se usa para convertir un tipo de valor en un tipo de valor nullable, el cual puede contener un valor nulo.

int? num = null;
if(num is int numValue)
{
	Console.WriteLine(numValue); 
}

Acceso condicional nulo a miembros (?. o ?[])

(!) (revisar abajo como usarlo mejor junto al operador ??) (!)

Este operador permite acceder a un miembro de un objeto solo si el objeto no es nulo, evitando asi una NullReferenceException. Si no se cumple, devuelve null.

  • si a es nulo, el resultado de a?.x o a?[x] es null
  • si a no es nulo, el resultado de a?.x o a?[x] es el mismo que a.x o a[x]
string[] nombres = null;
int? longitud = nombres?.Length; // asigna null en lugar de lanzar una excepcion

Coalescencia de Nulos (??)

Se usa para proporcionar un valor por defecto en caso de que una expresion nullable contenga un null.

sin este operador

// DON'T DO THIS
string name = GetName();
if(name == null)
{
	name = "unknown";
}

se reemplaza por esto usando el operador

// do this instead
string name = GetName() ?? "unknown";

Se recomienda usar ?? en vez de expresiones ternarias para comprobar un null

// DON'T DO THIS
var v = x == null ? x : y;

// do this instead
var v = x ?? y;

uso de operador .? junto a ??

Se puede usar el operador ?? junto a .? para asignar un valor por defecto, en caso de que alguna propiedad accedida mediante .? sea nula en algún punto

ejemplo 1

string[] nombres = // something 
string nombre = nombres?[index] ?? "NotFound"; // array is null or name not found

ejemplo 2

// if the value someList from request is null, this if will check as false
if ((request?.someList?.Count ?? 0) <= 0)
{
	// ... do something
}

También se puede usar junto a un throw para hacer más concisa la asignación de valores.

public string Name
{
	get => name;
	set => name = value ?? throw new ArgumentNullException(nameof(value), "Name cannot be null");
}

Asignacion de coalescencia de nulos (??=)

reemplaza este formato de código

if (variable is null) 
{
	variable = "something";
}

por esto. lo hace más conciso

variable ??= "something";

Reference(s)

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator
https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0029-ide0030-ide0270
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators–and-

Linq examples

Retrieve a simple subset of properties for all items in list

Usually, anon data types are used in the select clause to return a specific subset of properties for each object in the collection.

we have the employee class

public class Employee
{
	public int ID { get; set; }
	public string Name { get; set; }
	public int Age { get; set; }
	public string Address { get; set; }
}
List<Employee> employees = // ...

// we convert an Employee into an anon object type with a subset of properties
var employeeDetails = from emp in employees
	select new { Id = emp.ID, Name = emp.Name };

How to map List of classes

public List<MappedUser> MapListOfUsers(List<User> users)
{
	// method 1
	List<MappedUser> mappedUsers = users.ConvertAll(user => MapSingleUser(user));
	
	// method 2
	List<MappedUser> mappedUsers2 = 
		(from user in users select MapSingleUser(user)).ToList();
}

method to encapsulate mapping itself

private MappedUser MapSingleUser(User user)
{
 var mapped = new MappedUser
 {
	 Id = user.Id,
	 Name = user.Name,
	 Email = user.Email
 };
 return mapped;
}

This provides easier and more legible than doing a foreach to iterate everything.

How to filter list per properties (Where)

var adminUserTask = users
	.Where(user => "admin".Equals(user.type.ToLower()))
	.Select(async user => { return await ProcessAdmin(user);});
List<UserResults> results = (await Task.WhenAll(adminUserTask)).ToList();

filter by properties in a nullable list and return true if there’s any row that match. If the list is null, it returns false.

return response.results?.rows
	.Where(row => row.id == requestId && (row.owner == requestOwner || row.responsible == requestResponsible))
	.Any() ?? false;

another example

var task = response.results.rows
	.AsParallel()
	.Where(row => "specifictype".Equals(row.type.ToLower()))
	.Select(async row => {
		if(row.type.Equals("specificType"))
		{
			return await Something(row, id, log);
		} else 
		{
			return row;
		}
	});

How to select records based on another list (ids)

This is how to select a list of items, selecting them by id, based on another list of items

List<string> idList = // ...
var profiles = _context.UserProfiles
		.Where(userProf => idList.Contains(userProf.Id));

How to order based on a dynamic parameter

// Direction is an Enum w. values ASC or DESC
private List<Person> SortPersons(Direction direction, List<Person> persons, Func<Person, string> sortBy)
{
	if(direction.DESC)
	{
		return persons.OrderByDescending(sortBy.Invoke).ToList();
	} else 
	{
		return persons.OrderBy(sortBy.Invoke).ToList();
	}
}

How to use it

var sortedPersons = SortPersons(direction, persons, person => person.Name);

Single param lambda

remember that for lambdas with a single param where it comes from the same query, you don’t need to explicitely set it

this transforms

public List<int> FilterList(List<int> listToFilter, List<int> filterintList)
{
	return listToFilter.Where(number => filteringList.Contains(number)).ToList();
}

Methods worth a mention

Remove duplicates Distinct()

var task = response.results.rows
	.AsParallel()
	.Where(row => "specificType".Equals(row.type.ToLower()))
	.Select(async row => { return await Something(row, id, log);});

Get the difference of two lists Except()

var list = listToFilter
	.Except(filteringList)
	.ToList();

First() vs Single()

// gets the first element that matches and stops there
string list = listToFilter
	.First(s => s.StartsWith("ohno"));

// gets the matching element, or throws an exception if there's more than 1
string list = listToFilter
	.Single(s => s.StartsWith("ohno"));

Validaciones C#

C# tiene los DataAnnotation

[Required] hace que sea un campo obligatorio

[Required(ErrorMessage = "Nombre es obligatorio")]
public string NombreCategoria { get; set; }

[Required(ErrorMessage = "Orden es obligatorio")]
[Range(1, int.MaxValue, ErrorMessage = "El orden debe de ser mayor a cero")]
public int Orden {get; set; }

Se controla mediante el siguiente codigo en el controller. El código de ModelState es código base de un Controller.

if(ModelState.IsValid) 
{
	// code if everything's valid
}

C# coding style

Interfaces must start by capital I

public interface IDataService 
{ 
	public Task SendData(DataModel model);
}

We use PascalCase for:

  • classes’ name
  • methods’ name
  • public variables

We use CamelCase for:

  • private or internal field names (they must include the prefix _)
  • methods’ paramters
public class DataService
{
	const int TAX = 7;
	
	public bool IsValid { get; private set; };
	private IWorkerQueue _workerQueue;
	
	public async Task SendData(DataModel model)
	{
		string someValue = "";
		// ... whatever
	}
}

async methods must end by Async

public async Task<string> GetUrlAsync()
{
	// ... whatever
}

Reference(s)

https://learn.microsoft.com/es-es/dotnet/csharp/fundamentals/coding-style/identifier-names

Introducción a .NET

CLR (Common language runtime)

Entorno de ejecucion para .NET. En tiempo de ejecucion el compilador de CLR convierte el codigo CIL en codigo nativo para el SO. Facilita la integración entre lenguajes.

CLR es la MV en la que se ejecutan nuestras apps. CLR se hizo para tener una capa de abstraccion entre las propias apps y el SO donde se ejecutaban.

El CLI se puede ejecutar en otros SO. El CLR se ejecuta solo en Windows.

.NET intro

Read More

.NET launchsettings vs appsettings

launchSettings

NO se despliega. Afecta a tu entorno local.

En él establecemos los perfiles con los que ejecutaremos nuestro proyecto y se usa para definir la variable ASPNETCORE_ENVIRONMENT. También permite establecer la url y puertos de ejecución.

{
  "profiles": {
    "my.project.namespace": {
      "commandName": "Project",
      "launchBrowser": false,
      "applicationUrl": "https://localhost:5011;http://localhost:5010",
	  "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

ASPNETCORE_ENVIRONMENT Indica el entorno - soporta los siguientes valores

  • Development
  • Staging
  • Production valor por defecto si se omite el valor

appsettings

Se utiliza para almacenar la configuración de la aplicación como por ej. cadenas de conexión de BBDD.
Se utiliza tanto en entornos de desarrollo como de producción.

{
	"LocalDirectory": "/opt/data-download",
	"MyServiceConfig": {
		"Uri": "http://localhost:8800",
		"Endpoint": "/some-endpoint",
		"Timeout": 30
	}
}

appsettings.{Environment}.json

Son archivos adicionales opcionales, donde {Environment} corresponde al valor de la variable ASPNETCORE_ENVIRONMENT del launchSettings.json.

El orden de carga es:

  1. appsettings.json
  2. appsettings.{Environment}.json (si existe)
  3. launchSettings.json

Hay varias maneras de leer la configuración.

leer de la raiz

Las que se encuentran en la raiz las podemos leer inyectando la config en la clase.

public class MyService(IConfiguration _config) : IMyService
{
	public void MyMethod()
	{
		var localDirectory = _config["LocalDirectory"];
	}
}

leer de una clase de config custom

por un lado tenemos la clase de config

public class MyServiceConfig
{
	public const string Section = "MyServiceConfig";

	public string Uri { get; set; }
	public string Endpoint { get; set; }
	public int Timeout { get; set; }
}

y por otro lado la inyectamos

public class MyService(MyServiceConfig _config) : IMyService
{
	public void MyMethod()
	{
		var uri = _config.Uri;
		var endpoint = _config.Endpoint;
		var timeout = _config.Timeout;
	}
}

la tendremos que poner también en el Startup

public void ConfigureServices(IServiceCollection services)
{
	// ...
	services.Configure<MyServiceConfig>(config.GetSection(MyServiceConfig.Section));
	// ...
}

Buenas prácticas

Usar appsettings.json como base y en cada appsettings.{Environment].json poner solo los datos que cambien.

No poner secretos en appsettings.json ni sus variantes:

  • En local (para development) utilizar User secrets
  • Para los entornos con Kubernetes inyectar secretos via ConfigMaps o Secrets

Reference(s)

https://learn.microsoft.com/es-es/aspnet/core/fundamentals/environments?view=aspnetcore-8.0
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0