claude-agent-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
# Skill: Bases de Datos NoSQL
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Implementar y utilizar bases de datos NoSQL en aplicaciones Rails para casos de uso especificos donde SQL tradicional no es la mejor opcion.
|
|
6
|
+
|
|
7
|
+
## Tipos de Bases NoSQL
|
|
8
|
+
|
|
9
|
+
```markdown
|
|
10
|
+
| Tipo | Ejemplos | Casos de Uso |
|
|
11
|
+
|------|----------|--------------|
|
|
12
|
+
| Document | MongoDB, CouchDB | Datos semi-estructurados, CMS, catalogos |
|
|
13
|
+
| Key-Value | Redis, Memcached | Cache, sessions, colas |
|
|
14
|
+
| Column-Family | Cassandra, HBase | Time-series, analytics a gran escala |
|
|
15
|
+
| Graph | Neo4j, Amazon Neptune | Redes sociales, recomendaciones |
|
|
16
|
+
| Search | Elasticsearch, Solr | Full-text search, logs |
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## MongoDB
|
|
20
|
+
|
|
21
|
+
### Configuracion con Mongoid
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# Gemfile
|
|
25
|
+
gem "mongoid"
|
|
26
|
+
|
|
27
|
+
# Generar configuracion
|
|
28
|
+
# rails g mongoid:config
|
|
29
|
+
|
|
30
|
+
# config/mongoid.yml
|
|
31
|
+
development:
|
|
32
|
+
clients:
|
|
33
|
+
default:
|
|
34
|
+
database: myapp_development
|
|
35
|
+
hosts:
|
|
36
|
+
- localhost:27017
|
|
37
|
+
options:
|
|
38
|
+
server_selection_timeout: 5
|
|
39
|
+
max_pool_size: 5
|
|
40
|
+
|
|
41
|
+
production:
|
|
42
|
+
clients:
|
|
43
|
+
default:
|
|
44
|
+
uri: <%= ENV['MONGODB_URI'] %>
|
|
45
|
+
options:
|
|
46
|
+
ssl: true
|
|
47
|
+
ssl_verify: true
|
|
48
|
+
max_pool_size: 50
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Modelos con Mongoid
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# app/models/product.rb
|
|
55
|
+
class Product
|
|
56
|
+
include Mongoid::Document
|
|
57
|
+
include Mongoid::Timestamps
|
|
58
|
+
|
|
59
|
+
# Campos con tipos
|
|
60
|
+
field :name, type: String
|
|
61
|
+
field :sku, type: String
|
|
62
|
+
field :price, type: BigDecimal
|
|
63
|
+
field :stock_quantity, type: Integer, default: 0
|
|
64
|
+
field :active, type: Boolean, default: true
|
|
65
|
+
field :tags, type: Array, default: []
|
|
66
|
+
field :release_date, type: Date
|
|
67
|
+
|
|
68
|
+
# Documento embebido (almacenado dentro del producto)
|
|
69
|
+
embeds_many :variants
|
|
70
|
+
embeds_one :dimensions
|
|
71
|
+
|
|
72
|
+
# Referencia a otro documento (como foreign key)
|
|
73
|
+
belongs_to :category
|
|
74
|
+
has_many :reviews
|
|
75
|
+
|
|
76
|
+
# Indices
|
|
77
|
+
index({ sku: 1 }, { unique: true })
|
|
78
|
+
index({ name: "text", description: "text" })
|
|
79
|
+
index({ category_id: 1, active: 1 })
|
|
80
|
+
index({ tags: 1 })
|
|
81
|
+
|
|
82
|
+
# Validaciones
|
|
83
|
+
validates :name, presence: true
|
|
84
|
+
validates :sku, presence: true, uniqueness: true
|
|
85
|
+
validates :price, numericality: { greater_than: 0 }
|
|
86
|
+
|
|
87
|
+
# Scopes
|
|
88
|
+
scope :active, -> { where(active: true) }
|
|
89
|
+
scope :in_stock, -> { where(:stock_quantity.gt => 0) }
|
|
90
|
+
scope :by_category, ->(cat_id) { where(category_id: cat_id) }
|
|
91
|
+
scope :with_tag, ->(tag) { where(:tags.in => [tag]) }
|
|
92
|
+
|
|
93
|
+
# Callbacks
|
|
94
|
+
before_save :normalize_sku
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def normalize_sku
|
|
99
|
+
self.sku = sku.upcase.strip if sku.present?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# app/models/variant.rb (documento embebido)
|
|
104
|
+
class Variant
|
|
105
|
+
include Mongoid::Document
|
|
106
|
+
|
|
107
|
+
embedded_in :product
|
|
108
|
+
|
|
109
|
+
field :name, type: String
|
|
110
|
+
field :sku_suffix, type: String
|
|
111
|
+
field :price_modifier, type: BigDecimal, default: 0
|
|
112
|
+
field :stock_quantity, type: Integer, default: 0
|
|
113
|
+
field :attributes, type: Hash, default: {}
|
|
114
|
+
|
|
115
|
+
validates :name, presence: true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# app/models/dimensions.rb
|
|
119
|
+
class Dimensions
|
|
120
|
+
include Mongoid::Document
|
|
121
|
+
|
|
122
|
+
embedded_in :product
|
|
123
|
+
|
|
124
|
+
field :width, type: Float
|
|
125
|
+
field :height, type: Float
|
|
126
|
+
field :depth, type: Float
|
|
127
|
+
field :weight, type: Float
|
|
128
|
+
field :unit, type: String, default: "cm"
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Queries en MongoDB
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
# Busquedas basicas
|
|
136
|
+
Product.where(active: true)
|
|
137
|
+
Product.where(price: 10..100)
|
|
138
|
+
Product.where(:stock_quantity.gt => 0)
|
|
139
|
+
Product.where(:tags.in => ["electronics", "gadgets"])
|
|
140
|
+
|
|
141
|
+
# Busqueda en campos anidados
|
|
142
|
+
Product.where("dimensions.weight" => { "$lt" => 5 })
|
|
143
|
+
Product.where("variants.name" => "Large")
|
|
144
|
+
|
|
145
|
+
# Busqueda de texto
|
|
146
|
+
Product.text_search("wireless headphones")
|
|
147
|
+
|
|
148
|
+
# Operadores de comparacion
|
|
149
|
+
Product.where(:price.gt => 50)
|
|
150
|
+
Product.where(:price.gte => 50)
|
|
151
|
+
Product.where(:price.lt => 100)
|
|
152
|
+
Product.where(:price.lte => 100)
|
|
153
|
+
Product.where(:price.ne => 0)
|
|
154
|
+
|
|
155
|
+
# Operadores logicos
|
|
156
|
+
Product.or({ active: true }, { :stock_quantity.gt => 0 })
|
|
157
|
+
Product.and({ active: true }, { :price.lt => 100 })
|
|
158
|
+
Product.not.where(active: false)
|
|
159
|
+
|
|
160
|
+
# Operadores de array
|
|
161
|
+
Product.where(:tags.all => ["featured", "sale"])
|
|
162
|
+
Product.where(:tags.size => 3)
|
|
163
|
+
Product.where(:tags.elem_match => { "$regex" => /^tech/ })
|
|
164
|
+
|
|
165
|
+
# Proyeccion (seleccionar campos)
|
|
166
|
+
Product.only(:name, :price)
|
|
167
|
+
Product.without(:description, :metadata)
|
|
168
|
+
|
|
169
|
+
# Ordenamiento y paginacion
|
|
170
|
+
Product.order_by(price: :asc).skip(20).limit(10)
|
|
171
|
+
Product.order_by(created_at: :desc).first
|
|
172
|
+
|
|
173
|
+
# Aggregation
|
|
174
|
+
Product.collection.aggregate([
|
|
175
|
+
{ "$match" => { active: true } },
|
|
176
|
+
{ "$group" => {
|
|
177
|
+
"_id" => "$category_id",
|
|
178
|
+
"count" => { "$sum" => 1 },
|
|
179
|
+
"avg_price" => { "$avg" => "$price" }
|
|
180
|
+
}},
|
|
181
|
+
{ "$sort" => { "count" => -1 } }
|
|
182
|
+
])
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Aggregation Pipeline
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# app/services/product_analytics.rb
|
|
189
|
+
class ProductAnalytics
|
|
190
|
+
def self.sales_by_category(start_date:, end_date:)
|
|
191
|
+
Order.collection.aggregate([
|
|
192
|
+
# Filtrar por fecha
|
|
193
|
+
{ "$match" => {
|
|
194
|
+
"created_at" => {
|
|
195
|
+
"$gte" => start_date,
|
|
196
|
+
"$lte" => end_date
|
|
197
|
+
}
|
|
198
|
+
}},
|
|
199
|
+
|
|
200
|
+
# Descomponer array de items
|
|
201
|
+
{ "$unwind" => "$items" },
|
|
202
|
+
|
|
203
|
+
# Lookup para traer producto
|
|
204
|
+
{ "$lookup" => {
|
|
205
|
+
"from" => "products",
|
|
206
|
+
"localField" => "items.product_id",
|
|
207
|
+
"foreignField" => "_id",
|
|
208
|
+
"as" => "product"
|
|
209
|
+
}},
|
|
210
|
+
|
|
211
|
+
# Descomponer resultado del lookup
|
|
212
|
+
{ "$unwind" => "$product" },
|
|
213
|
+
|
|
214
|
+
# Agrupar por categoria
|
|
215
|
+
{ "$group" => {
|
|
216
|
+
"_id" => "$product.category_id",
|
|
217
|
+
"total_revenue" => {
|
|
218
|
+
"$sum" => { "$multiply" => ["$items.quantity", "$items.price"] }
|
|
219
|
+
},
|
|
220
|
+
"total_units" => { "$sum" => "$items.quantity" },
|
|
221
|
+
"order_count" => { "$sum" => 1 }
|
|
222
|
+
}},
|
|
223
|
+
|
|
224
|
+
# Lookup para nombre de categoria
|
|
225
|
+
{ "$lookup" => {
|
|
226
|
+
"from" => "categories",
|
|
227
|
+
"localField" => "_id",
|
|
228
|
+
"foreignField" => "_id",
|
|
229
|
+
"as" => "category"
|
|
230
|
+
}},
|
|
231
|
+
|
|
232
|
+
{ "$unwind" => "$category" },
|
|
233
|
+
|
|
234
|
+
# Proyectar resultado final
|
|
235
|
+
{ "$project" => {
|
|
236
|
+
"_id" => 0,
|
|
237
|
+
"category_name" => "$category.name",
|
|
238
|
+
"total_revenue" => 1,
|
|
239
|
+
"total_units" => 1,
|
|
240
|
+
"order_count" => 1,
|
|
241
|
+
"avg_order_value" => { "$divide" => ["$total_revenue", "$order_count"] }
|
|
242
|
+
}},
|
|
243
|
+
|
|
244
|
+
# Ordenar
|
|
245
|
+
{ "$sort" => { "total_revenue" => -1 } }
|
|
246
|
+
]).to_a
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Redis
|
|
252
|
+
|
|
253
|
+
### Configuracion
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# Gemfile
|
|
257
|
+
gem "redis"
|
|
258
|
+
gem "hiredis" # Driver nativo mas rapido
|
|
259
|
+
gem "connection_pool"
|
|
260
|
+
|
|
261
|
+
# config/initializers/redis.rb
|
|
262
|
+
require "connection_pool"
|
|
263
|
+
|
|
264
|
+
REDIS_POOL = ConnectionPool.new(size: 10, timeout: 5) do
|
|
265
|
+
Redis.new(
|
|
266
|
+
url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" },
|
|
267
|
+
driver: :hiredis
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Helper para acceso
|
|
272
|
+
def redis
|
|
273
|
+
REDIS_POOL.with { |conn| yield conn }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# O crear cliente global
|
|
277
|
+
REDIS = Redis.new(url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" })
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Strings
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# Set/Get basico
|
|
284
|
+
redis { |r| r.set("user:1:name", "John") }
|
|
285
|
+
redis { |r| r.get("user:1:name") } # => "John"
|
|
286
|
+
|
|
287
|
+
# Con expiracion
|
|
288
|
+
redis { |r| r.setex("session:abc123", 3600, user_data.to_json) } # Expira en 1 hora
|
|
289
|
+
|
|
290
|
+
# Solo si no existe
|
|
291
|
+
redis { |r| r.setnx("lock:order:123", "processing") }
|
|
292
|
+
|
|
293
|
+
# Incrementar/Decrementar
|
|
294
|
+
redis { |r| r.incr("visits:page:home") }
|
|
295
|
+
redis { |r| r.incrby("user:1:points", 10) }
|
|
296
|
+
redis { |r| r.decr("inventory:product:456") }
|
|
297
|
+
|
|
298
|
+
# Multiples operaciones
|
|
299
|
+
redis { |r|
|
|
300
|
+
r.mset("key1", "value1", "key2", "value2")
|
|
301
|
+
r.mget("key1", "key2") # => ["value1", "value2"]
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Hashes
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
# Almacenar objeto como hash
|
|
309
|
+
redis { |r|
|
|
310
|
+
r.hset("user:1", {
|
|
311
|
+
"name" => "John",
|
|
312
|
+
"email" => "john@example.com",
|
|
313
|
+
"age" => 30
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# Obtener campos
|
|
318
|
+
redis { |r| r.hget("user:1", "name") } # => "John"
|
|
319
|
+
redis { |r| r.hgetall("user:1") } # => {"name" => "John", ...}
|
|
320
|
+
redis { |r| r.hmget("user:1", "name", "email") }
|
|
321
|
+
|
|
322
|
+
# Incrementar campo numerico
|
|
323
|
+
redis { |r| r.hincrby("user:1", "login_count", 1) }
|
|
324
|
+
|
|
325
|
+
# Verificar existencia
|
|
326
|
+
redis { |r| r.hexists("user:1", "email") } # => true
|
|
327
|
+
|
|
328
|
+
# Patron para cache de objetos
|
|
329
|
+
class UserCache
|
|
330
|
+
def self.get(user_id)
|
|
331
|
+
data = REDIS.hgetall("user:#{user_id}")
|
|
332
|
+
return nil if data.empty?
|
|
333
|
+
|
|
334
|
+
User.new(data.symbolize_keys)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def self.set(user)
|
|
338
|
+
REDIS.hset("user:#{user.id}", user.cache_attributes)
|
|
339
|
+
REDIS.expire("user:#{user.id}", 1.hour.to_i)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def self.invalidate(user_id)
|
|
343
|
+
REDIS.del("user:#{user_id}")
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Lists
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# Cola FIFO
|
|
352
|
+
redis { |r| r.lpush("queue:emails", email_data.to_json) } # Agregar al inicio
|
|
353
|
+
redis { |r| r.rpop("queue:emails") } # Sacar del final
|
|
354
|
+
|
|
355
|
+
# Cola LIFO (stack)
|
|
356
|
+
redis { |r| r.lpush("stack:undo", action.to_json) }
|
|
357
|
+
redis { |r| r.lpop("stack:undo") }
|
|
358
|
+
|
|
359
|
+
# Obtener rango
|
|
360
|
+
redis { |r| r.lrange("recent:posts", 0, 9) } # Primeros 10
|
|
361
|
+
|
|
362
|
+
# Bloquear hasta que haya elementos (para workers)
|
|
363
|
+
redis { |r| r.brpop("queue:jobs", timeout: 30) }
|
|
364
|
+
|
|
365
|
+
# Limitar tamano de lista
|
|
366
|
+
redis { |r|
|
|
367
|
+
r.lpush("user:1:notifications", notification.to_json)
|
|
368
|
+
r.ltrim("user:1:notifications", 0, 99) # Mantener solo 100
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Timeline/Feed
|
|
372
|
+
class UserFeed
|
|
373
|
+
def self.add_post(user_id, post_id)
|
|
374
|
+
followers = User.find(user_id).follower_ids
|
|
375
|
+
|
|
376
|
+
REDIS.pipelined do |pipe|
|
|
377
|
+
followers.each do |follower_id|
|
|
378
|
+
pipe.lpush("feed:#{follower_id}", post_id)
|
|
379
|
+
pipe.ltrim("feed:#{follower_id}", 0, 999)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def self.get_feed(user_id, page: 1, per_page: 20)
|
|
385
|
+
start = (page - 1) * per_page
|
|
386
|
+
stop = start + per_page - 1
|
|
387
|
+
|
|
388
|
+
post_ids = REDIS.lrange("feed:#{user_id}", start, stop)
|
|
389
|
+
Post.where(id: post_ids).order_by_ids(post_ids)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Sets
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
# Agregar a set
|
|
398
|
+
redis { |r| r.sadd("tags:product:1", ["electronics", "gadgets", "tech"]) }
|
|
399
|
+
|
|
400
|
+
# Verificar pertenencia
|
|
401
|
+
redis { |r| r.sismember("tags:product:1", "electronics") } # => true
|
|
402
|
+
|
|
403
|
+
# Obtener todos los miembros
|
|
404
|
+
redis { |r| r.smembers("tags:product:1") }
|
|
405
|
+
|
|
406
|
+
# Operaciones de conjuntos
|
|
407
|
+
redis { |r| r.sinter("tags:product:1", "tags:product:2") } # Interseccion
|
|
408
|
+
redis { |r| r.sunion("tags:product:1", "tags:product:2") } # Union
|
|
409
|
+
redis { |r| r.sdiff("tags:product:1", "tags:product:2") } # Diferencia
|
|
410
|
+
|
|
411
|
+
# Contar miembros
|
|
412
|
+
redis { |r| r.scard("online:users") }
|
|
413
|
+
|
|
414
|
+
# Usuarios online
|
|
415
|
+
class OnlineTracker
|
|
416
|
+
def self.mark_online(user_id)
|
|
417
|
+
REDIS.sadd("online:users", user_id)
|
|
418
|
+
REDIS.expire("online:users", 5.minutes.to_i)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def self.online_count
|
|
422
|
+
REDIS.scard("online:users")
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def self.is_online?(user_id)
|
|
426
|
+
REDIS.sismember("online:users", user_id)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def self.online_friends(user_id)
|
|
430
|
+
friend_ids = User.find(user_id).friend_ids
|
|
431
|
+
REDIS.sinter("online:users", friend_ids)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Sorted Sets
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
# Agregar con score
|
|
440
|
+
redis { |r| r.zadd("leaderboard", 1000, "user:1") }
|
|
441
|
+
redis { |r| r.zadd("leaderboard", 850, "user:2") }
|
|
442
|
+
|
|
443
|
+
# Incrementar score
|
|
444
|
+
redis { |r| r.zincrby("leaderboard", 50, "user:1") }
|
|
445
|
+
|
|
446
|
+
# Ranking (mayor a menor)
|
|
447
|
+
redis { |r| r.zrevrange("leaderboard", 0, 9, with_scores: true) }
|
|
448
|
+
|
|
449
|
+
# Obtener rank de usuario
|
|
450
|
+
redis { |r| r.zrevrank("leaderboard", "user:1") } # 0-based
|
|
451
|
+
|
|
452
|
+
# Rango por score
|
|
453
|
+
redis { |r| r.zrangebyscore("leaderboard", 500, 1000) }
|
|
454
|
+
|
|
455
|
+
# Leaderboard completo
|
|
456
|
+
class Leaderboard
|
|
457
|
+
def initialize(name)
|
|
458
|
+
@name = "leaderboard:#{name}"
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def add_score(user_id, score)
|
|
462
|
+
REDIS.zincrby(@name, score, user_id)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def top(count = 10)
|
|
466
|
+
REDIS.zrevrange(@name, 0, count - 1, with_scores: true).map do |user_id, score|
|
|
467
|
+
{ user_id: user_id, score: score.to_i }
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def rank_for(user_id)
|
|
472
|
+
rank = REDIS.zrevrank(@name, user_id)
|
|
473
|
+
rank ? rank + 1 : nil
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def score_for(user_id)
|
|
477
|
+
REDIS.zscore(@name, user_id)&.to_i
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def around_user(user_id, range: 5)
|
|
481
|
+
rank = REDIS.zrevrank(@name, user_id)
|
|
482
|
+
return [] unless rank
|
|
483
|
+
|
|
484
|
+
start = [0, rank - range].max
|
|
485
|
+
stop = rank + range
|
|
486
|
+
|
|
487
|
+
REDIS.zrevrange(@name, start, stop, with_scores: true).map.with_index do |(uid, score), idx|
|
|
488
|
+
{ rank: start + idx + 1, user_id: uid, score: score.to_i }
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Pub/Sub
|
|
495
|
+
|
|
496
|
+
```ruby
|
|
497
|
+
# Publicador
|
|
498
|
+
class EventPublisher
|
|
499
|
+
def self.publish(channel, event)
|
|
500
|
+
REDIS.publish("events:#{channel}", event.to_json)
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Suscriptor (en proceso separado)
|
|
505
|
+
class EventSubscriber
|
|
506
|
+
def self.subscribe(*channels)
|
|
507
|
+
redis = Redis.new(url: ENV["REDIS_URL"])
|
|
508
|
+
|
|
509
|
+
redis.subscribe(*channels.map { |c| "events:#{c}" }) do |on|
|
|
510
|
+
on.subscribe do |channel, subscriptions|
|
|
511
|
+
puts "Subscribed to #{channel} (#{subscriptions} subscriptions)"
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
on.message do |channel, message|
|
|
515
|
+
event = JSON.parse(message)
|
|
516
|
+
handle_event(channel, event)
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def self.handle_event(channel, event)
|
|
522
|
+
case channel
|
|
523
|
+
when "events:orders"
|
|
524
|
+
OrderEventHandler.process(event)
|
|
525
|
+
when "events:users"
|
|
526
|
+
UserEventHandler.process(event)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Real-time notifications con Action Cable
|
|
532
|
+
class NotificationChannel < ApplicationCable::Channel
|
|
533
|
+
def subscribed
|
|
534
|
+
# Suscribirse a Redis channel
|
|
535
|
+
redis_subscribe("notifications:#{current_user.id}")
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
private
|
|
539
|
+
|
|
540
|
+
def redis_subscribe(channel)
|
|
541
|
+
Thread.new do
|
|
542
|
+
Redis.new.subscribe(channel) do |on|
|
|
543
|
+
on.message do |_channel, message|
|
|
544
|
+
transmit(JSON.parse(message))
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## Neo4j
|
|
553
|
+
|
|
554
|
+
### Configuracion
|
|
555
|
+
|
|
556
|
+
```ruby
|
|
557
|
+
# Gemfile
|
|
558
|
+
gem "neo4j"
|
|
559
|
+
gem "neo4j-core"
|
|
560
|
+
|
|
561
|
+
# config/neo4j.yml
|
|
562
|
+
development:
|
|
563
|
+
type: bolt
|
|
564
|
+
url: bolt://localhost:7687
|
|
565
|
+
username: neo4j
|
|
566
|
+
password: password
|
|
567
|
+
|
|
568
|
+
production:
|
|
569
|
+
type: bolt
|
|
570
|
+
url: <%= ENV['NEO4J_URL'] %>
|
|
571
|
+
username: <%= ENV['NEO4J_USERNAME'] %>
|
|
572
|
+
password: <%= ENV['NEO4J_PASSWORD'] %>
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Modelos
|
|
576
|
+
|
|
577
|
+
```ruby
|
|
578
|
+
# app/models/person.rb
|
|
579
|
+
class Person
|
|
580
|
+
include Neo4j::ActiveNode
|
|
581
|
+
|
|
582
|
+
property :name, type: String
|
|
583
|
+
property :email, type: String
|
|
584
|
+
property :born, type: Integer
|
|
585
|
+
|
|
586
|
+
has_many :out, :friends, rel_class: :Friendship, model_class: :Person
|
|
587
|
+
has_many :in, :followers, rel_class: :Follows, model_class: :Person
|
|
588
|
+
has_many :out, :following, rel_class: :Follows, model_class: :Person
|
|
589
|
+
has_many :out, :posts, type: :AUTHORED
|
|
590
|
+
has_many :out, :liked_posts, rel_class: :Likes, model_class: :Post
|
|
591
|
+
|
|
592
|
+
validates :name, presence: true
|
|
593
|
+
validates :email, presence: true, uniqueness: true
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# app/models/post.rb
|
|
597
|
+
class Post
|
|
598
|
+
include Neo4j::ActiveNode
|
|
599
|
+
|
|
600
|
+
property :title, type: String
|
|
601
|
+
property :body, type: String
|
|
602
|
+
property :created_at, type: DateTime
|
|
603
|
+
|
|
604
|
+
has_one :in, :author, type: :AUTHORED, model_class: :Person
|
|
605
|
+
has_many :in, :likers, rel_class: :Likes, model_class: :Person
|
|
606
|
+
has_many :out, :tags, type: :TAGGED_WITH
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# app/models/friendship.rb (relacion con propiedades)
|
|
610
|
+
class Friendship
|
|
611
|
+
include Neo4j::ActiveRel
|
|
612
|
+
|
|
613
|
+
from_class :Person
|
|
614
|
+
to_class :Person
|
|
615
|
+
type :FRIENDS_WITH
|
|
616
|
+
|
|
617
|
+
property :since, type: Date
|
|
618
|
+
property :closeness, type: Integer # 1-10
|
|
619
|
+
|
|
620
|
+
validates :since, presence: true
|
|
621
|
+
end
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Cypher Queries
|
|
625
|
+
|
|
626
|
+
```ruby
|
|
627
|
+
# Encontrar amigos de amigos
|
|
628
|
+
def friends_of_friends(person)
|
|
629
|
+
Neo4j::ActiveBase.new_query
|
|
630
|
+
.match("(p:Person)-[:FRIENDS_WITH]->(friend)-[:FRIENDS_WITH]->(fof:Person)")
|
|
631
|
+
.where(p: { uuid: person.uuid })
|
|
632
|
+
.where_not("(p)-[:FRIENDS_WITH]->(fof)")
|
|
633
|
+
.where_not("p = fof")
|
|
634
|
+
.return("DISTINCT fof")
|
|
635
|
+
.to_a
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Camino mas corto entre dos personas
|
|
639
|
+
def shortest_path(person1, person2)
|
|
640
|
+
Neo4j::ActiveBase.new_query
|
|
641
|
+
.match("path = shortestPath((p1:Person)-[:FRIENDS_WITH*]-(p2:Person))")
|
|
642
|
+
.where(p1: { uuid: person1.uuid })
|
|
643
|
+
.where(p2: { uuid: person2.uuid })
|
|
644
|
+
.return("path, length(path) as distance")
|
|
645
|
+
.first
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# Recomendaciones basadas en grafo
|
|
649
|
+
def recommend_friends(person, limit: 10)
|
|
650
|
+
Neo4j::ActiveBase.new_query
|
|
651
|
+
.match("(p:Person)-[:FRIENDS_WITH]->(friend)-[:FRIENDS_WITH]->(recommendation:Person)")
|
|
652
|
+
.where(p: { uuid: person.uuid })
|
|
653
|
+
.where_not("(p)-[:FRIENDS_WITH]->(recommendation)")
|
|
654
|
+
.where_not("p = recommendation")
|
|
655
|
+
.return("recommendation, COUNT(friend) as mutual_friends")
|
|
656
|
+
.order("mutual_friends DESC")
|
|
657
|
+
.limit(limit)
|
|
658
|
+
.to_a
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Posts que podrian gustarle al usuario
|
|
662
|
+
def recommend_posts(person, limit: 20)
|
|
663
|
+
Neo4j::ActiveBase.new_query
|
|
664
|
+
.match("(p:Person)-[:FRIENDS_WITH]->(friend)-[:LIKES]->(post:Post)")
|
|
665
|
+
.where(p: { uuid: person.uuid })
|
|
666
|
+
.where_not("(p)-[:LIKES]->(post)")
|
|
667
|
+
.with("post, COUNT(friend) as friend_likes")
|
|
668
|
+
.order("friend_likes DESC")
|
|
669
|
+
.limit(limit)
|
|
670
|
+
.return("post, friend_likes")
|
|
671
|
+
.to_a
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Influencers (personas con mas conexiones)
|
|
675
|
+
def top_influencers(limit: 10)
|
|
676
|
+
Neo4j::ActiveBase.new_query
|
|
677
|
+
.match("(p:Person)<-[:FOLLOWS]-(follower:Person)")
|
|
678
|
+
.with("p, COUNT(follower) as followers")
|
|
679
|
+
.order("followers DESC")
|
|
680
|
+
.limit(limit)
|
|
681
|
+
.return("p, followers")
|
|
682
|
+
.to_a
|
|
683
|
+
end
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
## Elasticsearch
|
|
687
|
+
|
|
688
|
+
### Configuracion
|
|
689
|
+
|
|
690
|
+
```ruby
|
|
691
|
+
# Gemfile
|
|
692
|
+
gem "elasticsearch-model"
|
|
693
|
+
gem "elasticsearch-rails"
|
|
694
|
+
|
|
695
|
+
# config/initializers/elasticsearch.rb
|
|
696
|
+
Elasticsearch::Model.client = Elasticsearch::Client.new(
|
|
697
|
+
url: ENV.fetch("ELASTICSEARCH_URL") { "http://localhost:9200" },
|
|
698
|
+
log: Rails.env.development?
|
|
699
|
+
)
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Modelo con Elasticsearch
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
# app/models/article.rb
|
|
706
|
+
class Article < ApplicationRecord
|
|
707
|
+
include Elasticsearch::Model
|
|
708
|
+
include Elasticsearch::Model::Callbacks # Sync automatico
|
|
709
|
+
|
|
710
|
+
# Configurar indice
|
|
711
|
+
settings index: {
|
|
712
|
+
number_of_shards: 1,
|
|
713
|
+
analysis: {
|
|
714
|
+
analyzer: {
|
|
715
|
+
spanish_analyzer: {
|
|
716
|
+
type: "spanish",
|
|
717
|
+
stopwords: "_spanish_"
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} do
|
|
722
|
+
mappings dynamic: "false" do
|
|
723
|
+
indexes :title, type: "text", analyzer: "spanish_analyzer", boost: 2
|
|
724
|
+
indexes :body, type: "text", analyzer: "spanish_analyzer"
|
|
725
|
+
indexes :tags, type: "keyword"
|
|
726
|
+
indexes :author_name, type: "text"
|
|
727
|
+
indexes :category, type: "keyword"
|
|
728
|
+
indexes :published_at, type: "date"
|
|
729
|
+
indexes :views_count, type: "integer"
|
|
730
|
+
indexes :location, type: "geo_point"
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Definir que datos indexar
|
|
735
|
+
def as_indexed_json(options = {})
|
|
736
|
+
{
|
|
737
|
+
title: title,
|
|
738
|
+
body: body,
|
|
739
|
+
tags: tags,
|
|
740
|
+
author_name: author.name,
|
|
741
|
+
category: category.name,
|
|
742
|
+
published_at: published_at,
|
|
743
|
+
views_count: views_count,
|
|
744
|
+
location: { lat: latitude, lon: longitude }
|
|
745
|
+
}
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# Metodos de busqueda
|
|
749
|
+
def self.search_query(query, filters = {})
|
|
750
|
+
search_definition = {
|
|
751
|
+
query: {
|
|
752
|
+
bool: {
|
|
753
|
+
must: [
|
|
754
|
+
{
|
|
755
|
+
multi_match: {
|
|
756
|
+
query: query,
|
|
757
|
+
fields: ["title^2", "body", "author_name"],
|
|
758
|
+
type: "best_fields",
|
|
759
|
+
fuzziness: "AUTO"
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
],
|
|
763
|
+
filter: build_filters(filters)
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
highlight: {
|
|
767
|
+
fields: {
|
|
768
|
+
title: {},
|
|
769
|
+
body: { fragment_size: 150 }
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
aggs: {
|
|
773
|
+
categories: { terms: { field: "category" } },
|
|
774
|
+
tags: { terms: { field: "tags", size: 20 } }
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
__elasticsearch__.search(search_definition)
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
private
|
|
782
|
+
|
|
783
|
+
def self.build_filters(filters)
|
|
784
|
+
result = []
|
|
785
|
+
|
|
786
|
+
if filters[:category].present?
|
|
787
|
+
result << { term: { category: filters[:category] } }
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
if filters[:tags].present?
|
|
791
|
+
result << { terms: { tags: Array(filters[:tags]) } }
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
if filters[:date_range].present?
|
|
795
|
+
result << {
|
|
796
|
+
range: {
|
|
797
|
+
published_at: {
|
|
798
|
+
gte: filters[:date_range][:from],
|
|
799
|
+
lte: filters[:date_range][:to]
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
result
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
### Queries avanzadas
|
|
811
|
+
|
|
812
|
+
```ruby
|
|
813
|
+
# app/services/article_search.rb
|
|
814
|
+
class ArticleSearch
|
|
815
|
+
def initialize(params)
|
|
816
|
+
@query = params[:q]
|
|
817
|
+
@page = params[:page] || 1
|
|
818
|
+
@per_page = params[:per_page] || 20
|
|
819
|
+
@filters = params.slice(:category, :tags, :author)
|
|
820
|
+
@sort = params[:sort]
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def call
|
|
824
|
+
Article.__elasticsearch__.search(search_body)
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
private
|
|
828
|
+
|
|
829
|
+
def search_body
|
|
830
|
+
{
|
|
831
|
+
query: build_query,
|
|
832
|
+
from: (@page - 1) * @per_page,
|
|
833
|
+
size: @per_page,
|
|
834
|
+
sort: build_sort,
|
|
835
|
+
aggs: aggregations,
|
|
836
|
+
highlight: highlights,
|
|
837
|
+
suggest: suggestions
|
|
838
|
+
}
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def build_query
|
|
842
|
+
{
|
|
843
|
+
bool: {
|
|
844
|
+
must: must_clauses,
|
|
845
|
+
filter: filter_clauses,
|
|
846
|
+
should: boost_clauses
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def must_clauses
|
|
852
|
+
return [{ match_all: {} }] if @query.blank?
|
|
853
|
+
|
|
854
|
+
[
|
|
855
|
+
{
|
|
856
|
+
multi_match: {
|
|
857
|
+
query: @query,
|
|
858
|
+
fields: ["title^3", "body", "tags^2"],
|
|
859
|
+
type: "cross_fields",
|
|
860
|
+
operator: "and"
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
]
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
def filter_clauses
|
|
867
|
+
filters = []
|
|
868
|
+
|
|
869
|
+
filters << { term: { category: @filters[:category] } } if @filters[:category]
|
|
870
|
+
filters << { terms: { tags: @filters[:tags] } } if @filters[:tags]
|
|
871
|
+
filters << { term: { "author_name.keyword": @filters[:author] } } if @filters[:author]
|
|
872
|
+
|
|
873
|
+
filters
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def boost_clauses
|
|
877
|
+
[
|
|
878
|
+
{ range: { published_at: { gte: "now-7d", boost: 2 } } }, # Articulos recientes
|
|
879
|
+
{ range: { views_count: { gte: 1000, boost: 1.5 } } } # Populares
|
|
880
|
+
]
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def build_sort
|
|
884
|
+
case @sort
|
|
885
|
+
when "recent"
|
|
886
|
+
[{ published_at: "desc" }]
|
|
887
|
+
when "popular"
|
|
888
|
+
[{ views_count: "desc" }]
|
|
889
|
+
else
|
|
890
|
+
["_score", { published_at: "desc" }]
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
def aggregations
|
|
895
|
+
{
|
|
896
|
+
categories: {
|
|
897
|
+
terms: { field: "category", size: 10 }
|
|
898
|
+
},
|
|
899
|
+
popular_tags: {
|
|
900
|
+
terms: { field: "tags", size: 20 }
|
|
901
|
+
},
|
|
902
|
+
date_histogram: {
|
|
903
|
+
date_histogram: {
|
|
904
|
+
field: "published_at",
|
|
905
|
+
calendar_interval: "month"
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def highlights
|
|
912
|
+
{
|
|
913
|
+
pre_tags: ["<mark>"],
|
|
914
|
+
post_tags: ["</mark>"],
|
|
915
|
+
fields: {
|
|
916
|
+
title: { number_of_fragments: 0 },
|
|
917
|
+
body: { fragment_size: 200, number_of_fragments: 3 }
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def suggestions
|
|
923
|
+
return {} if @query.blank?
|
|
924
|
+
|
|
925
|
+
{
|
|
926
|
+
title_suggestion: {
|
|
927
|
+
text: @query,
|
|
928
|
+
term: { field: "title" }
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
end
|
|
932
|
+
end
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
## DynamoDB
|
|
936
|
+
|
|
937
|
+
### Configuracion
|
|
938
|
+
|
|
939
|
+
```ruby
|
|
940
|
+
# Gemfile
|
|
941
|
+
gem "aws-sdk-dynamodb"
|
|
942
|
+
gem "dynamoid" # ORM para DynamoDB
|
|
943
|
+
|
|
944
|
+
# config/initializers/dynamodb.rb
|
|
945
|
+
Dynamoid.configure do |config|
|
|
946
|
+
config.namespace = "myapp_#{Rails.env}"
|
|
947
|
+
config.region = ENV.fetch("AWS_REGION") { "us-east-1" }
|
|
948
|
+
config.access_key = ENV["AWS_ACCESS_KEY_ID"]
|
|
949
|
+
config.secret_key = ENV["AWS_SECRET_ACCESS_KEY"]
|
|
950
|
+
end
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
### Modelos con Dynamoid
|
|
954
|
+
|
|
955
|
+
```ruby
|
|
956
|
+
# app/models/session.rb
|
|
957
|
+
class Session
|
|
958
|
+
include Dynamoid::Document
|
|
959
|
+
|
|
960
|
+
table name: :sessions, key: :session_id
|
|
961
|
+
|
|
962
|
+
field :session_id, :string
|
|
963
|
+
field :user_id, :string
|
|
964
|
+
field :data, :serialized
|
|
965
|
+
field :expires_at, :datetime
|
|
966
|
+
|
|
967
|
+
global_secondary_index hash_key: :user_id, projected_attributes: :all
|
|
968
|
+
|
|
969
|
+
# TTL para auto-expiracion
|
|
970
|
+
field :ttl, :integer
|
|
971
|
+
|
|
972
|
+
before_create :set_ttl
|
|
973
|
+
|
|
974
|
+
private
|
|
975
|
+
|
|
976
|
+
def set_ttl
|
|
977
|
+
self.ttl = (expires_at || 1.day.from_now).to_i
|
|
978
|
+
end
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
# app/models/event.rb
|
|
982
|
+
class Event
|
|
983
|
+
include Dynamoid::Document
|
|
984
|
+
|
|
985
|
+
table name: :events, key: :event_id, range_key: :timestamp
|
|
986
|
+
|
|
987
|
+
field :event_id, :string
|
|
988
|
+
field :timestamp, :datetime
|
|
989
|
+
field :event_type, :string
|
|
990
|
+
field :user_id, :string
|
|
991
|
+
field :data, :map
|
|
992
|
+
field :metadata, :map
|
|
993
|
+
|
|
994
|
+
global_secondary_index hash_key: :user_id,
|
|
995
|
+
range_key: :timestamp,
|
|
996
|
+
projected_attributes: :all
|
|
997
|
+
|
|
998
|
+
global_secondary_index hash_key: :event_type,
|
|
999
|
+
range_key: :timestamp,
|
|
1000
|
+
projected_attributes: :keys_only
|
|
1001
|
+
end
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Queries en DynamoDB
|
|
1005
|
+
|
|
1006
|
+
```ruby
|
|
1007
|
+
# Buscar por primary key
|
|
1008
|
+
session = Session.find("session-123")
|
|
1009
|
+
|
|
1010
|
+
# Query por GSI
|
|
1011
|
+
events = Event.where(user_id: "user-123")
|
|
1012
|
+
.where("timestamp.gt": 1.day.ago)
|
|
1013
|
+
|
|
1014
|
+
# Scan con filtro (costoso, evitar en produccion)
|
|
1015
|
+
sessions = Session.scan_filter(
|
|
1016
|
+
:user_id => { eq: "user-123" },
|
|
1017
|
+
:expires_at => { gt: Time.current }
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
# Batch operations
|
|
1021
|
+
Session.import([session1, session2, session3])
|
|
1022
|
+
Session.find_all(["id1", "id2", "id3"])
|
|
1023
|
+
|
|
1024
|
+
# Operaciones condicionales
|
|
1025
|
+
session.update_attributes(
|
|
1026
|
+
{ data: new_data },
|
|
1027
|
+
conditions: { if: { version: current_version } }
|
|
1028
|
+
)
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
## Cuando Usar NoSQL vs SQL
|
|
1032
|
+
|
|
1033
|
+
```markdown
|
|
1034
|
+
## Usar SQL cuando:
|
|
1035
|
+
- Necesitas transacciones ACID complejas
|
|
1036
|
+
- Datos altamente relacionados
|
|
1037
|
+
- Queries ad-hoc complejos
|
|
1038
|
+
- Reporting y analytics
|
|
1039
|
+
- Schema estricto es importante
|
|
1040
|
+
|
|
1041
|
+
## Usar MongoDB cuando:
|
|
1042
|
+
- Schema flexible/evolutivo
|
|
1043
|
+
- Documentos anidados complejos
|
|
1044
|
+
- Datos semi-estructurados
|
|
1045
|
+
- Prototipado rapido
|
|
1046
|
+
- Catalogos de productos
|
|
1047
|
+
|
|
1048
|
+
## Usar Redis cuando:
|
|
1049
|
+
- Cache de alto rendimiento
|
|
1050
|
+
- Sessions
|
|
1051
|
+
- Colas y pub/sub
|
|
1052
|
+
- Leaderboards
|
|
1053
|
+
- Rate limiting
|
|
1054
|
+
- Datos efimeros
|
|
1055
|
+
|
|
1056
|
+
## Usar Elasticsearch cuando:
|
|
1057
|
+
- Full-text search
|
|
1058
|
+
- Log analytics
|
|
1059
|
+
- Busqueda facetada
|
|
1060
|
+
- Autocompletado
|
|
1061
|
+
- Geo-search
|
|
1062
|
+
|
|
1063
|
+
## Usar Neo4j cuando:
|
|
1064
|
+
- Redes sociales
|
|
1065
|
+
- Sistemas de recomendacion
|
|
1066
|
+
- Deteccion de fraude
|
|
1067
|
+
- Knowledge graphs
|
|
1068
|
+
- Path finding
|
|
1069
|
+
|
|
1070
|
+
## Usar DynamoDB cuando:
|
|
1071
|
+
- Escala masiva (millones de requests/segundo)
|
|
1072
|
+
- Latencia consistente
|
|
1073
|
+
- Serverless
|
|
1074
|
+
- Key-value simple con indices
|
|
1075
|
+
- Gaming/IoT
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
## Patrones Hibridos
|
|
1079
|
+
|
|
1080
|
+
```ruby
|
|
1081
|
+
# Usar SQL como fuente de verdad + Redis para cache
|
|
1082
|
+
class Product < ApplicationRecord
|
|
1083
|
+
after_commit :invalidate_cache
|
|
1084
|
+
|
|
1085
|
+
def self.featured
|
|
1086
|
+
Rails.cache.fetch("products:featured", expires_in: 1.hour) do
|
|
1087
|
+
where(featured: true).limit(10).to_a
|
|
1088
|
+
end
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
private
|
|
1092
|
+
|
|
1093
|
+
def invalidate_cache
|
|
1094
|
+
Rails.cache.delete("products:featured")
|
|
1095
|
+
end
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
# Usar SQL + Elasticsearch para busqueda
|
|
1099
|
+
class Product < ApplicationRecord
|
|
1100
|
+
include Elasticsearch::Model
|
|
1101
|
+
|
|
1102
|
+
# SQL para transacciones y relaciones
|
|
1103
|
+
# Elasticsearch para busqueda
|
|
1104
|
+
|
|
1105
|
+
def self.search(query)
|
|
1106
|
+
if query.present?
|
|
1107
|
+
__elasticsearch__.search(query).records
|
|
1108
|
+
else
|
|
1109
|
+
all
|
|
1110
|
+
end
|
|
1111
|
+
end
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
# Usar SQL + MongoDB para diferentes partes
|
|
1115
|
+
class Order < ApplicationRecord
|
|
1116
|
+
# Datos transaccionales en PostgreSQL
|
|
1117
|
+
has_many :line_items
|
|
1118
|
+
|
|
1119
|
+
# Logs/eventos en MongoDB
|
|
1120
|
+
def log_event(event_type, data = {})
|
|
1121
|
+
OrderEvent.create!(
|
|
1122
|
+
order_id: id.to_s,
|
|
1123
|
+
event_type: event_type,
|
|
1124
|
+
data: data,
|
|
1125
|
+
timestamp: Time.current
|
|
1126
|
+
)
|
|
1127
|
+
end
|
|
1128
|
+
end
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
## Checklist
|
|
1132
|
+
|
|
1133
|
+
- [ ] Evaluar si NoSQL es apropiado para el caso de uso
|
|
1134
|
+
- [ ] Considerar patron hibrido SQL + NoSQL
|
|
1135
|
+
- [ ] Configurar conexiones con pooling
|
|
1136
|
+
- [ ] Implementar manejo de errores y reintentos
|
|
1137
|
+
- [ ] Configurar indices apropiados
|
|
1138
|
+
- [ ] Establecer TTLs para datos temporales
|
|
1139
|
+
- [ ] Monitorear rendimiento y uso de memoria
|
|
1140
|
+
- [ ] Planificar estrategia de backup
|
|
1141
|
+
- [ ] Documentar decisiones de modelado
|