# Guía de Elasticsearch

Esta guía cubre el trabajo con Elasticsearch en SLM Framework.

## Descripción General

SLM Framework utiliza Elasticsearch como su almacén de datos principal. Todas las entidades (usuarios, perfiles, elementos, etc.) se almacenan como documentos en índices de Elasticsearch.

## Estructura de Índices

### Índice Principal

Almacena todos los datos de la aplicación:

```
slm-app-dev/
├── users (tipo: user)
├── profiles (tipo: profile)
├── items (tipo: item)
└── ... (tipos personalizados)
```

### Índice de Logs

Almacena logs de la aplicación:

```
slm-app-dev-log/
└── logs (tipo: log)
```

### Índice de Sesiones

Almacena sesiones de usuario:

```
sessions/
└── documentos de sesión
```

## Estructura de Documentos

Todos los documentos siguen esta estructura:

```json
{
  "name": "Elemento de Ejemplo",
  "status": "active",
  "campo_personalizado": "valor",
  "slm": {
    "type": "item",
    "timestamp": 1704067200000,
    "created_by": "user_id",
    "updated_by": "user_id"
  }
}
```

### El Objeto `slm`

Cada documento debe tener un objeto `slm`:

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `type` | keyword | Identificador del tipo de documento |
| `timestamp` | date | Marca de tiempo de creación/actualización (milisegundos) |
| `created_by` | keyword | ID del usuario que creó |
| `updated_by` | keyword | ID del usuario que actualizó |

## Creando Índices

### Mapping del Índice Principal

```bash
curl -X PUT "http://localhost:9200/slm-app-dev" -H "Content-Type: application/json" -d '{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "slm": {
        "properties": {
          "type": { "type": "keyword" },
          "timestamp": { "type": "date", "format": "epoch_millis" },
          "created_by": { "type": "keyword" },
          "updated_by": { "type": "keyword" }
        }
      },
      "name": {
        "type": "text",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "email": {
        "type": "text",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "status": { "type": "keyword" },
      "description": { "type": "text" },
      "price": { "type": "float" },
      "category": { "type": "keyword" },
      "tags": { "type": "keyword" }
    }
  }
}'
```

### Mapping del Índice de Logs

```bash
curl -X PUT "http://localhost:9200/slm-app-dev-log" -H "Content-Type: application/json" -d '{
  "mappings": {
    "properties": {
      "log_type": { "type": "keyword" },
      "module": { "type": "keyword" },
      "script": { "type": "keyword" },
      "user_id": { "type": "keyword" },
      "user_email": { "type": "keyword" },
      "data": { "type": "text" },
      "debug": { "type": "text" },
      "slm": {
        "properties": {
          "type": { "type": "keyword" },
          "timestamp": { "type": "date", "format": "epoch_millis" },
          "date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }
        }
      }
    }
  }
}'
```

## Operaciones CRUD

### Crear Documento

```php
$data = [
    'name' => 'Nuevo Elemento',
    'status' => 'active',
    'slm' => [
        'type' => 'item',
        'timestamp' => getTimestamp(),
        'created_by' => _SLM::$session['user']['id']
    ]
];

$result = setNewObject($data);

// Verificar resultado
if (isset($result['error'])) {
    throw new Exception($result['error']['reason']);
}

$newId = $result['_id'];
```

### Leer Documento por ID

```php
$item = getObjectById($id);

if (!$item) {
    throw new RuntimeException('Elemento no encontrado');
}

// Excluir campos sensibles
$user = getObjectById($id, '_source_excludes=password');
```

### Leer Documento por Valor de Campo

```php
$user = getObjectWithValue('user', 'email.keyword', 'usuario@ejemplo.com');

if (!$user) {
    throw new RuntimeException('Usuario no encontrado');
}

// El resultado incluye el campo 'id' mezclado con _source
$userId = $user['id'];
```

### Actualizar Documento

```php
$updateData = [
    'name' => 'Nombre Actualizado',
    'slm' => [
        'type' => 'item',
        'timestamp' => getTimestamp(),
        'updated_by' => _SLM::$session['user']['id']
    ]
];

$result = updateObjectByID($id, $updateData);

if (isset($result['error'])) {
    throw new Exception($result['error']['reason']);
}
```

### Eliminar Documento (Eliminación Suave)

```php
$result = updateObjectByID($id, [
    'status' => 'inactive',
    'slm' => [
        'type' => 'item',
        'timestamp' => getTimestamp(),
        'deleted_by' => _SLM::$session['user']['id']
    ]
]);
```

### Eliminar Documento (Eliminación Permanente)

```php
$result = json_decode(es_execute(
    "/" . getEsIndex() . "/_doc/$id?refresh=true",
    "",
    "DELETE"
), true);
```

## Operaciones de Búsqueda

### Búsqueda Básica

```php
$es_index = getEsIndex();
$query = "(slm.type:item)AND(status.keyword:active)";

$result = json_decode(es_execute(
    "/$es_index/_search?q=$query&size=100"
), true);

$items = $result['hits']['hits'];
foreach ($items as $hit) {
    $item = $hit['_source'];
    $item['id'] = $hit['_id'];
}
```

### Búsqueda con Ordenamiento

```php
$result = json_decode(es_execute(
    "/$es_index/_search?q=$query&sort=name.keyword:asc&size=50"
), true);
```

### Búsqueda con Paginación

```php
$from = ($page - 1) * $pageSize;
$result = json_decode(es_execute(
    "/$es_index/_search?q=$query&from=$from&size=$pageSize"
), true);

$total = $result['hits']['total']['value'];
```

### Búsqueda con Selección de Campos

```php
$result = json_decode(es_execute(
    "/$es_index/_search?q=$query&_source=name,status"
), true);
```

### Contar Documentos

```php
$result = json_decode(es_execute(
    "/$es_index/_count?q=$query"
), true);

$count = $result['count'];
```

## Procesamiento del Lado del Servidor (SSP)

Para integración con DataTables:

```php
$searchTerm = $_POST['search']['value'] ?? '';
$start = (int)($_POST['start'] ?? 0);
$length = (int)($_POST['length'] ?? 10);

// Condiciones base de consulta
$search = ['(slm.type:item)', '(status.keyword:active)'];

// Campos buscables
$fields = ['name.keyword', 'description'];

// Parámetros
$params = [
    'from' => $start,
    'size' => $length,
    'sort' => 'slm.timestamp',
    'order' => 'desc'
];

$ssp = new SSP($searchTerm, $search, $fields, $params);
$result = $ssp->setEsIndex(getEsIndex())->setReturn(true)->run();

// Formato del resultado:
// ['recordsTotal' => 100, 'recordsFiltered' => 50, 'data' => [...]]
```

## Sintaxis de Query String

### Operadores Booleanos

```
AND: (field1:value1)AND(field2:value2)
OR:  (field1:value1)OR(field2:value2)
NOT: NOT(field:value)
```

### Comodines

```
name:*test*     # Contiene "test"
name:test*      # Empieza con "test"
name:*test      # Termina con "test"
```

### Consultas de Rango

```
price:[10 TO 100]     # 10 a 100 inclusivo
price:{10 TO 100}     # 10 a 100 exclusivo
timestamp:[1704067200000 TO *]  # >= timestamp
```

### Escape de Caracteres Especiales

```php
$value = escapeEsValue($userInput);
// Escapa: + - = & | > < ! ( ) { } [ ] ^ " ~ * ? : \ /
```

## Mejores Prácticas

### 1. Usa Campos Keyword para Coincidencias Exactas

```php
// Correcto - coincidencia exacta en campo keyword
$query = "(email.keyword:\"usuario@ejemplo.com\")";

// Incorrecto - campo text puede no coincidir exactamente
$query = "(email:usuario@ejemplo.com)";
```

### 2. Siempre Verifica Errores

```php
$result = setNewObject($data);

if (isset($result['error'])) {
    error_log("Error ES: " . $result['error']['reason']);
    throw new Exception('Error de base de datos');
}

if ($result['result'] !== 'created') {
    throw new Exception('Error al crear documento');
}
```

### 3. Usa Tipos de Campo Apropiados

| Caso de Uso | Tipo de Campo |
|-------------|---------------|
| Coincidencia exacta, filtros | `keyword` |
| Búsqueda de texto completo | `text` |
| Números para rangos | `integer`, `float` |
| Fechas | `date` con formato |
| Booleano | `boolean` |

### 4. Refrescar Después de Escritura

El parámetro `?refresh=true` asegura que el documento sea inmediatamente buscable:

```php
es_execute("/$es_index/_doc/$id?refresh=true", $json, "POST");
```

### 5. Excluir Datos Sensibles

```php
$user = getObjectById($id, '_source_excludes=password,secret');
```

## Kibana

Accede a Kibana en http://localhost:5601 para:

- Explorar índices y documentos
- Ejecutar consultas
- Crear visualizaciones
- Monitorear salud del cluster

## Solución de Problemas

### Índice No Encontrado

```bash
# Verificar si el índice existe
curl "http://localhost:9200/_cat/indices?v"

# Crear índice si no existe
curl -X PUT "http://localhost:9200/slm-app-dev"
```

### Conflictos de Mapping

```bash
# Verificar mapping actual
curl "http://localhost:9200/slm-app-dev/_mapping"

# Reindexar si es necesario
curl -X POST "http://localhost:9200/_reindex" -H "Content-Type: application/json" -d '{
  "source": { "index": "old-index" },
  "dest": { "index": "new-index" }
}'
```

### Problemas de Conexión

```php
// Verificar conexión
try {
    $result = es_execute("/_cluster/health");
    print_r(json_decode($result, true));
} catch (Exception $e) {
    echo "Conexión fallida: " . $e->getMessage();
}
```
