rospatent 1.2.0 → 1.3.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.
data/README.md CHANGED
@@ -15,7 +15,7 @@ A comprehensive Ruby client for the Rospatent patent search API with advanced fe
15
15
  - 📊 **Structured Logging** - JSON/text logging with request/response tracking
16
16
  - 🚀 **Batch Operations** - Process multiple patents concurrently
17
17
  - ⚙️ **Environment-Aware** - Different configurations for dev/staging/production
18
- - 🧪 **Comprehensive Testing** - 96% test coverage with integration tests
18
+ - 🧪 **Comprehensive Testing** - 219 tests with 465 assertions, comprehensive integration testing
19
19
  - 📚 **Excellent Documentation** - Detailed examples and API documentation
20
20
 
21
21
  ## Installation
@@ -41,7 +41,7 @@ $ gem install rospatent
41
41
  ## Quick Start
42
42
 
43
43
  ```ruby
44
- # Basic configuration
44
+ # Minimal configuration
45
45
  Rospatent.configure do |config|
46
46
  config.token = "your_jwt_token"
47
47
  end
@@ -101,25 +101,149 @@ Rospatent.configure do |config|
101
101
  end
102
102
  ```
103
103
 
104
- ### Environment Variables
104
+ ### Environment-Specific Configuration
105
105
 
106
- Configure via environment variables:
106
+ The gem automatically adjusts settings based on environment with sensible defaults:
107
+
108
+ #### Development Environment
109
+
110
+ ```ruby
111
+ # Optimized for development
112
+ Rospatent.configure do |config|
113
+ config.environment = "development"
114
+ config.token = ENV['ROSPATENT_TOKEN']
115
+ config.log_level = :debug
116
+ config.log_requests = true
117
+ config.log_responses = true
118
+ config.cache_ttl = 60 # Short cache for development
119
+ config.timeout = 10 # Fast timeouts for quick feedback
120
+ end
121
+ ```
122
+
123
+ #### Staging Environment
124
+
125
+ ```ruby
126
+ # Optimized for staging
127
+ Rospatent.configure do |config|
128
+ config.environment = "staging"
129
+ config.token = ENV['ROSPATENT_TOKEN']
130
+ config.log_level = :info
131
+ config.cache_ttl = 300 # Longer cache for performance
132
+ config.timeout = 45 # Longer timeouts for reliability
133
+ config.retry_count = 3 # More retries for resilience
134
+ end
135
+ ```
136
+
137
+ #### Production Environment
138
+
139
+ ```ruby
140
+ # Optimized for production
141
+ Rospatent.configure do |config|
142
+ config.environment = "production"
143
+ config.token = ENV['ROSPATENT_TOKEN']
144
+ config.log_level = :warn
145
+ config.cache_ttl = 600 # Longer cache for performance
146
+ config.timeout = 60 # Longer timeouts for reliability
147
+ config.retry_count = 5 # More retries for resilience
148
+ end
149
+ ```
150
+
151
+ ### Environment Variables & Rails Integration
152
+
153
+ ⚠️ **CRITICAL**: Understanding environment variable priority is essential to avoid configuration issues, especially in Rails applications.
154
+
155
+ #### Token Configuration Priority
156
+
157
+ 1. **Rails credentials**: `Rails.application.credentials.rospatent_token`
158
+ 2. **Primary environment variable**: `ROSPATENT_TOKEN`
159
+ 3. **Legacy environment variable**: `ROSPATENT_API_TOKEN`
107
160
 
108
161
  ```bash
109
- ROSPATENT_ENV=production
110
- ROSPATENT_CACHE_ENABLED=true
111
- ROSPATENT_CACHE_TTL=600
112
- ROSPATENT_LOG_LEVEL=info
113
- ROSPATENT_POOL_SIZE=10
162
+ # Recommended approach
163
+ export ROSPATENT_TOKEN="your_jwt_token"
164
+
165
+ # Legacy support (still works)
166
+ export ROSPATENT_API_TOKEN="your_jwt_token"
167
+ ```
168
+
169
+ #### Log Level Configuration Priority
170
+
171
+ ```ruby
172
+ # Environment variable takes precedence over Rails defaults
173
+ config.log_level = if ENV.key?("ROSPATENT_LOG_LEVEL")
174
+ ENV["ROSPATENT_LOG_LEVEL"].to_sym
175
+ else
176
+ Rails.env.production? ? :warn : :debug
177
+ end
114
178
  ```
115
179
 
116
- ### Environment-Specific Defaults
180
+ ⚠️ **Common Issue**: Setting `ROSPATENT_LOG_LEVEL=debug` in production will override Rails-specific logic and cause DEBUG logs to appear in production!
181
+
182
+ #### Complete Environment Variables Reference
183
+
184
+ ```bash
185
+ # Core configuration
186
+ ROSPATENT_TOKEN="your_jwt_token" # API authentication token
187
+ ROSPATENT_ENV="production" # Override Rails.env if needed
188
+ ROSPATENT_API_URL="custom_url" # Override default API URL
189
+
190
+ # Logging configuration
191
+ ROSPATENT_LOG_LEVEL="warn" # debug, info, warn, error
192
+ ROSPATENT_LOG_REQUESTS="false" # Log API requests
193
+ ROSPATENT_LOG_RESPONSES="false" # Log API responses
194
+
195
+ # Cache configuration
196
+ ROSPATENT_CACHE_ENABLED="true" # Enable/disable caching
197
+ ROSPATENT_CACHE_TTL="300" # Cache TTL in seconds
198
+ ROSPATENT_CACHE_MAX_SIZE="1000" # Maximum cache items
199
+
200
+ # Connection configuration
201
+ ROSPATENT_TIMEOUT="30" # Request timeout in seconds
202
+ ROSPATENT_RETRY_COUNT="3" # Number of retries
203
+ ROSPATENT_POOL_SIZE="5" # Connection pool size
204
+ ROSPATENT_KEEP_ALIVE="true" # Keep-alive connections
205
+
206
+ # Environment-specific overrides
207
+ ROSPATENT_DEV_API_URL="dev_url" # Development API URL
208
+ ROSPATENT_STAGING_API_URL="staging_url" # Staging API URL
209
+ ```
210
+
211
+ #### Best Practices for Rails
212
+
213
+ 1. **Use Rails credentials for tokens**:
214
+ ```bash
215
+ rails credentials:edit
216
+ # Add: rospatent_token: your_jwt_token
217
+ ```
218
+
219
+ 2. **Set environment-specific variables**:
220
+ ```bash
221
+ # config/environments/production.rb
222
+ ENV["ROSPATENT_LOG_LEVEL"] ||= "warn"
223
+ ENV["ROSPATENT_CACHE_ENABLED"] ||= "true"
224
+ ```
225
+
226
+ 3. **Avoid setting DEBUG level in production**:
227
+ ```bash
228
+ # ❌ DON'T DO THIS in production
229
+ export ROSPATENT_LOG_LEVEL=debug
230
+
231
+ # ✅ DO THIS instead
232
+ export ROSPATENT_LOG_LEVEL=warn
233
+ ```
117
234
 
118
- The gem automatically adjusts settings based on environment:
235
+ ### Configuration Validation
119
236
 
120
- - **Development**: Fast timeouts, verbose logging, short cache TTL
121
- - **Staging**: Moderate settings for testing
122
- - **Production**: Longer timeouts, optimized for reliability
237
+ ```ruby
238
+ # Validate current configuration
239
+ errors = Rospatent.validate_configuration
240
+ if errors.any?
241
+ puts "Configuration errors:"
242
+ errors.each { |error| puts " - #{error}" }
243
+ else
244
+ puts "Configuration is valid ✓"
245
+ end
246
+ ```
123
247
 
124
248
  ## Basic Usage
125
249
 
@@ -137,20 +261,66 @@ results = client.search(qn: "rocket engine design")
137
261
  # Advanced search with all options
138
262
  results = client.search(
139
263
  q: "ракета AND двигатель",
140
- limit: 20,
141
- offset: 0,
264
+ limit: 50,
265
+ offset: 100,
266
+ datasets: ["ru_since_1994"],
142
267
  filter: {
143
268
  "classification.ipc_group": { "values": ["F02K9"] },
144
- "biblio.application_date": { "from": "2020-01-01" }
269
+ "application.filing_date": { "range": { "gte": "20200101" } }
145
270
  },
146
- sort: :pub_date,
147
- group_by: :patent_family,
271
+ sort: "publication_date:desc", # same as 'sort: :pub_date'; see Search#validate_sort_parameter for other sort options
272
+ group_by: "family:dwpi", # Patent family grouping: "family:docdb" or "family:dwpi"
148
273
  include_facets: true,
149
- highlight: true,
274
+ pre_tag: "<mark>", # Both pre_tag and post_tag must be provided together
275
+ post_tag: "</mark>", # Can be strings or arrays for multi-color highlighting
276
+ highlight: { # Advanced highlight configuration (independent of pre_tag/post_tag)
277
+ "profiles" => [
278
+ { "q" => "космическая", "pre_tag" => "<b>", "post_tag" => "</b>" },
279
+ "_searchquery_"
280
+ ]
281
+ }
282
+ )
283
+
284
+ # Simple highlighting with tags (both pre_tag and post_tag required)
285
+ results = client.search(
286
+ q: "ракета",
150
287
  pre_tag: "<mark>",
151
288
  post_tag: "</mark>"
152
289
  )
153
290
 
291
+ # Multi-color highlighting with arrays
292
+ results = client.search(
293
+ q: "космическая ракета",
294
+ pre_tag: ["<b>", "<i>"], # Round-robin highlighting
295
+ post_tag: ["</b>", "</i>"] # with different tags
296
+ )
297
+
298
+ # Advanced highlighting with profiles (independent of pre_tag/post_tag)
299
+ results = client.search(
300
+ q: "ракета",
301
+ highlight: {
302
+ "profiles" => [
303
+ { "q" => "космическая", "pre_tag" => "<b>", "post_tag" => "</b>" },
304
+ "_searchquery_" # References main search query highlighting
305
+ ]
306
+ }
307
+ )
308
+
309
+ # Patent family grouping (groups patents from the same invention)
310
+ results = client.search(
311
+ q: "rocket",
312
+ group_by: "family:docdb", # DOCDB simple patent families
313
+ datasets: ["dwpi"],
314
+ limit: 10
315
+ )
316
+
317
+ results = client.search(
318
+ q: "rocket",
319
+ group_by: "family:dwpi", # DWPI simple patent families
320
+ datasets: ["dwpi"],
321
+ limit: 10
322
+ )
323
+
154
324
  # Process results
155
325
  puts "Found #{results.total} total results (#{results.available} available)"
156
326
  puts "Showing #{results.count} results"
@@ -158,12 +328,153 @@ puts "Showing #{results.count} results"
158
328
  results.hits.each do |hit|
159
329
  puts "ID: #{hit['id']}"
160
330
  puts "Title: #{hit.dig('biblio', 'ru', 'title')}"
161
- puts "Date: #{hit.dig('biblio', 'publication_date')}"
162
- puts "IPC: #{hit.dig('classification', 'ipc')}"
331
+ puts "Date: #{hit.dig('common', 'publication_date')}"
332
+ puts "IPC: #{hit.dig('common', 'classification', 'ipc')&.map {|c| c['fullname']}&.join('; ')}"
163
333
  puts "---"
164
334
  end
165
335
  ```
166
336
 
337
+ ### Advanced Filter Parameters
338
+
339
+ The `filter` parameter supports complex filtering with automatic validation and format conversion:
340
+
341
+ #### List Filters (require `{"values": [...]}` format)
342
+
343
+ ```ruby
344
+ # Classification filters
345
+ results = client.search(
346
+ q: "artificial intelligence",
347
+ filter: {
348
+ "classification.ipc_group": { "values": ["G06N", "G06F"] },
349
+ "classification.cpc_group": { "values": ["G06N3/", "G06N20/"] }
350
+ }
351
+ )
352
+
353
+ # Author and patent holder filters
354
+ results = client.search(
355
+ q: "invention",
356
+ filter: {
357
+ "authors": { "values": ["Иванов И.И.", "Петров П.П."] },
358
+ "patent_holders": { "values": ["ООО Компания"] },
359
+ "country": { "values": ["RU", "US"] },
360
+ "kind": { "values": ["A1", "U1"] }
361
+ }
362
+ )
363
+
364
+ # Document ID filters
365
+ results = client.search(
366
+ q: "device",
367
+ filter: {
368
+ "ids": { "values": ["RU134694U1_20131120", "RU2358138C1_20090610"] }
369
+ }
370
+ )
371
+ ```
372
+
373
+ #### Date Range Filters (require `{"range": {"operator": "YYYYMMDD"}}` format)
374
+
375
+ ```ruby
376
+ # Automatic date format conversion
377
+ results = client.search(
378
+ q: "innovation",
379
+ filter: {
380
+ "date_published": { "range": { "gte": "2020-01-01", "lte": "2023-12-31" } },
381
+ "application.filing_date": { "range": { "gte": "2019-06-15" } }
382
+ }
383
+ )
384
+
385
+ # Direct API format (YYYYMMDD)
386
+ results = client.search(
387
+ q: "technology",
388
+ filter: {
389
+ "date_published": { "range": { "gte": "20200101", "lt": "20240101" } }
390
+ }
391
+ )
392
+
393
+ # Using Date objects (automatically converted)
394
+ results = client.search(
395
+ q: "patent",
396
+ filter: {
397
+ "application.filing_date": {
398
+ "range": {
399
+ "gte": Date.new(2020, 1, 1),
400
+ "lte": Date.new(2023, 12, 31)
401
+ }
402
+ }
403
+ }
404
+ )
405
+ ```
406
+
407
+ **Supported date operators**: `gt`, `gte`, `lt`, `lte`
408
+
409
+ **Date format conversion**:
410
+ - `"2020-01-01"` → `"20200101"`
411
+ - `Date.new(2020, 1, 1)` → `"20200101"`
412
+ - `"20200101"` → `"20200101"` (no change)
413
+
414
+ #### Complex Multi-Field Filters
415
+
416
+ ```ruby
417
+ # Comprehensive filter example
418
+ results = client.search(
419
+ q: "машинное обучение",
420
+ filter: {
421
+ # List filters
422
+ "classification.ipc_group": { "values": ["G06N", "G06F"] },
423
+ "country": { "values": ["RU", "US", "CN"] },
424
+ "kind": { "values": ["A1", "A2"] },
425
+ "authors": { "values": ["Иванов И.И."] },
426
+
427
+ # Date range filters
428
+ "date_published": { "range": { "gte": "2020-01-01", "lte": "2023-12-31" } },
429
+ "application.filing_date": { "range": { "gte": "2019-01-01" } }
430
+ },
431
+ limit: 50
432
+ )
433
+ ```
434
+
435
+ **Supported Filter Fields**:
436
+
437
+ *List filters (require `{"values": [...]}` format):*
438
+ - `authors` - Patent authors
439
+ - `patent_holders` - Patent holders/assignees
440
+ - `country` - Country codes
441
+ - `kind` - Document types
442
+ - `ids` - Specific document IDs
443
+ - `classification.ipc*` - IPC classification codes
444
+ - `classification.cpc*` - CPC classification codes
445
+
446
+ *Date filters (require `{"range": {"operator": "YYYYMMDD"}}` format):*
447
+ - `date_published` - Publication date
448
+ - `application.filing_date` - Application filing date
449
+
450
+ **Filter Validation**:
451
+ - ✅ Automatic field name validation
452
+ - ✅ Structure validation (list vs range format)
453
+ - ✅ Date format conversion and validation
454
+ - ✅ Operator validation for ranges
455
+ - ✅ Helpful error messages for invalid filters
456
+
457
+ ```ruby
458
+ # These will raise ValidationError with specific messages:
459
+ client.search(
460
+ q: "test",
461
+ filter: { "invalid_field": { "values": ["test"] } }
462
+ )
463
+ # Error: "Invalid filter field: invalid_field"
464
+
465
+ client.search(
466
+ q: "test",
467
+ filter: { "authors": ["direct", "array"] } # Missing {"values": [...]} wrapper
468
+ )
469
+ # Error: "Filter 'authors' requires format: {\"values\": [...]}"
470
+
471
+ client.search(
472
+ q: "test",
473
+ filter: { "date_published": { "range": { "invalid_op": "20200101" } } }
474
+ )
475
+ # Error: "Invalid range operator: invalid_op. Supported: gt, gte, lt, lte"
476
+ ```
477
+
167
478
  ### Retrieving Patent Documents
168
479
 
169
480
  ```ruby
@@ -172,7 +483,7 @@ patent_doc = client.patent("RU134694U1_20131120")
172
483
 
173
484
  # Get patent by components
174
485
  patent_doc = client.patent_by_components(
175
- "RU", # country_code
486
+ "RU", # country_code
176
487
  "134694", # number
177
488
  "U1", # doc_type
178
489
  Date.new(2013, 11, 20) # date (String or Date object)
@@ -257,12 +568,24 @@ cpc_info = client.classification_code("cpc", code: "B63H11/00", lang: "en")
257
568
  - `"ru"` - Russian
258
569
  - `"en"` - English
259
570
 
571
+ ### Available datasets list
572
+
573
+ ```ruby
574
+ datasets = client.datasets_tree
575
+ datasets.each do |category|
576
+ puts "Category: #{category['name_en']}"
577
+ category.children.each do |dataset|
578
+ puts " #{dataset['id']}: #{dataset['name_en']}"
579
+ end
580
+ end
581
+ ```
582
+
260
583
  ### Media and Documents
261
584
 
262
585
  ```ruby
263
586
  # Download patent PDF
264
587
  pdf_data = client.patent_media(
265
- "National", # collection_id
588
+ "National", # collection_id
266
589
  "RU", # country_code
267
590
  "U1", # doc_type
268
591
  "2013/11/20", # pub_date
@@ -277,14 +600,6 @@ pdf_data = client.patent_media_by_id(
277
600
  "National",
278
601
  "document.pdf"
279
602
  )
280
-
281
- # Get available datasets
282
- datasets.each do |category|
283
- puts "Category: #{category['name_en']}"
284
- category.children.each do |dataset|
285
- puts " #{dataset['id']}: #{dataset['name_en']}"
286
- end
287
- end
288
603
  ```
289
604
 
290
605
  ## Advanced Features
@@ -363,7 +678,7 @@ shared_logger = Rospatent.shared_logger(level: :debug)
363
678
 
364
679
  ### Error Handling
365
680
 
366
- Comprehensive error handling with specific error types:
681
+ Comprehensive error handling with specific error types and improved error message extraction:
367
682
 
368
683
  ```ruby
369
684
  begin
@@ -385,6 +700,13 @@ rescue Rospatent::Errors::ConnectionError => e
385
700
  puts "Connection error: #{e.message}"
386
701
  puts "Original error: #{e.original_error}"
387
702
  end
703
+
704
+ # Enhanced error message extraction
705
+ # The client automatically extracts error messages from various API response formats:
706
+ # - {"result": "Error message"} (Rospatent API format)
707
+ # - {"error": "Error message"} (Standard format)
708
+ # - {"message": "Error message"} (Alternative format)
709
+ # - {"details": "Validation details"} (Validation errors)
388
710
  ```
389
711
 
390
712
  ### Input Validation
@@ -424,64 +746,6 @@ puts "Cache enabled: #{global_stats[:configuration][:cache_enabled]}"
424
746
  puts "API URL: #{global_stats[:configuration][:api_url]}"
425
747
  ```
426
748
 
427
- ## Environment Configuration
428
-
429
- ### Development Environment
430
-
431
- ```ruby
432
- # Optimized for development
433
- Rospatent.configure do |config|
434
- config.environment = "development"
435
- config.token = ENV['ROSPATENT_DEV_TOKEN']
436
- config.log_level = :debug
437
- config.log_requests = true
438
- config.log_responses = true
439
- config.cache_ttl = 60 # Short cache for development
440
- config.timeout = 10 # Fast timeouts for quick feedback
441
- end
442
- ```
443
-
444
- ### Staging Environment
445
-
446
- ```ruby
447
- # Optimized for staging
448
- Rospatent.configure do |config|
449
- config.environment = "staging"
450
- config.token = ENV['ROSPATENT_TOKEN']
451
- config.log_level = :info
452
- config.cache_ttl = 300 # Longer cache for performance
453
- config.timeout = 45 # Longer timeouts for reliability
454
- config.retry_count = 3 # More retries for resilience
455
- end
456
- ```
457
-
458
- ### Production Environment
459
-
460
- ```ruby
461
- # Optimized for production
462
- Rospatent.configure do |config|
463
- config.environment = "production"
464
- config.token = ENV['ROSPATENT_TOKEN']
465
- config.log_level = :warn
466
- config.cache_ttl = 600 # Longer cache for performance
467
- config.timeout = 60 # Longer timeouts for reliability
468
- config.retry_count = 5 # More retries for resilience
469
- end
470
- ```
471
-
472
- ### Configuration Validation
473
-
474
- ```ruby
475
- # Validate current configuration
476
- errors = Rospatent.validate_configuration
477
- if errors.any?
478
- puts "Configuration errors:"
479
- errors.each { |error| puts " - #{error}" }
480
- else
481
- puts "Configuration is valid ✓"
482
- end
483
- ```
484
-
485
749
  ## Rails Integration
486
750
 
487
751
  ### Generator
@@ -494,10 +758,23 @@ This creates `config/initializers/rospatent.rb`:
494
758
 
495
759
  ```ruby
496
760
  Rospatent.configure do |config|
497
- config.token = Rails.application.credentials.rospatent_token
498
- config.environment = Rails.env
761
+ # Token priority: Rails credentials > ROSPATENT_TOKEN > ROSPATENT_API_TOKEN
762
+ config.token = Rails.application.credentials.rospatent_token ||
763
+ ENV["ROSPATENT_TOKEN"] ||
764
+ ENV["ROSPATENT_API_TOKEN"]
765
+
766
+ # Environment configuration respects ROSPATENT_ENV
767
+ config.environment = ENV.fetch("ROSPATENT_ENV", Rails.env)
768
+
769
+ # CRITICAL: Environment variables take priority over Rails defaults
770
+ # This prevents DEBUG logs appearing in production if ROSPATENT_LOG_LEVEL=debug is set
771
+ config.log_level = if ENV.key?("ROSPATENT_LOG_LEVEL")
772
+ ENV["ROSPATENT_LOG_LEVEL"].to_sym
773
+ else
774
+ Rails.env.production? ? :warn : :debug
775
+ end
776
+
499
777
  config.cache_enabled = Rails.env.production?
500
- config.log_level = Rails.env.production? ? :warn : :debug
501
778
  end
502
779
  ```
503
780
 
@@ -617,6 +894,7 @@ The library uses **Faraday** as the HTTP client with redirect support for all en
617
894
 
618
895
  ⚠️ **Minor server-side limitations**:
619
896
  - **Similar Patents by Text**: Occasionally returns `503 Service Unavailable` (a server-side issue, not a client implementation issue)
897
+
620
898
  ⚠️ **Documentation inconsistencies**:
621
899
  - **Similar Patents**: According to the documentation, the array of hits is named `hits`, but the real implementation uses the name `data`
622
900
  - **Available Datasets**: The `name` key in the real implementation has the localization suffix — `name_ru`, `name_en`
@@ -681,10 +959,7 @@ $ bundle exec rake cache:clear
681
959
  $ bundle exec rake doc
682
960
 
683
961
  # Run integration tests
684
- $ bundle exec rake test_integration
685
-
686
- # Performance benchmarks
687
- $ bundle exec rake benchmark
962
+ $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN='<your_jwt_token>' bundle exec rake test_integration
688
963
 
689
964
  # Setup development environment
690
965
  $ bundle exec rake setup
@@ -826,12 +1101,10 @@ $ bundle exec rake release
826
1101
  - 📊 **Структурированное логирование** - JSON/текстовое логирование с отслеживанием запросов/ответов
827
1102
  - 🚀 **Пакетные операции** - параллельная обработка множества патентов
828
1103
  - ⚙️ **Адаптивные окружения** - различные конфигурации для development/staging/production
829
- - 🧪 **Комплексное тестирование** - 96% покрытие тестами с интеграционными тестами
1104
+ - 🧪 **Комплексное тестирование** - 219 тестов с 465 проверками, комплексное интеграционное тестирование
830
1105
  - 📚 **Отличная документация** - подробные примеры и документация API
831
1106
 
832
- ## Быстрый старт
833
-
834
- ### Установка
1107
+ ## Установка
835
1108
 
836
1109
  Добавьте в ваш Gemfile:
837
1110
 
@@ -839,70 +1112,497 @@ $ bundle exec rake release
839
1112
  gem 'rospatent'
840
1113
  ```
841
1114
 
1115
+ Затем выполните:
1116
+
1117
+ ```bash
1118
+ $ bundle install
1119
+ ```
842
1120
  Или установите напрямую:
843
1121
 
844
1122
  ```bash
845
1123
  $ gem install rospatent
846
1124
  ```
847
1125
 
848
- ### Базовая настройка
1126
+ ## Быстрый старт
849
1127
 
850
1128
  ```ruby
851
- require 'rospatent'
852
-
853
- # Настройка клиента
1129
+ # Минимальная конфигурация
854
1130
  Rospatent.configure do |config|
855
1131
  config.token = "ваш_jwt_токен"
856
1132
  end
857
1133
 
858
- # Создание клиента
1134
+ # Создание клиента и поиск
859
1135
  client = Rospatent.client
1136
+ results = client.search(q: "ракета", limit: 10)
1137
+
1138
+ puts "Найдено #{results.total} результатов"
1139
+ results.hits.each do |hit|
1140
+ puts "Патент: #{hit['id']} - #{hit.dig('biblio', 'ru', 'title')}"
1141
+ end
860
1142
  ```
861
1143
 
862
- ## Основное использование
1144
+ ## Конфигурация
863
1145
 
864
- ### Поиск патентов
1146
+ ### Базовая настройка
865
1147
 
866
1148
  ```ruby
867
- # Простой поиск
868
- results = client.search(q: "солнечная батарея")
1149
+ Rospatent.configure do |config|
1150
+ # Обязательно
1151
+ config.token = "ваш_jwt_токен"
869
1152
 
870
- # Расширенный поиск с фильтрами
871
- results = client.search(
872
- q: "искусственный интеллект",
873
- limit: 50,
874
- offset: 100,
875
- datasets: ["ru_since_1994"],
876
- sort: "pub_date:desc",
877
- highlight: true
878
- )
1153
+ # Настройки API
1154
+ config.api_url = "https://searchplatform.rospatent.gov.ru/patsearch/v0.2"
1155
+ config.timeout = 30
1156
+ config.retry_count = 3
879
1157
 
880
- # Обработка результатов
881
- puts "Найдено: #{results.total} патентов"
882
- results.hits.each do |patent|
883
- puts "#{patent['id']}: #{patent['title']}"
1158
+ # Окружение (development, staging, production)
1159
+ config.environment = "production"
884
1160
  end
885
1161
  ```
886
1162
 
887
- ### Получение документов патентов
1163
+ ### Продвинутая настройка
888
1164
 
889
1165
  ```ruby
890
- # По идентификатору документа
891
- patent = client.patent("RU134694U1_20131120")
1166
+ Rospatent.configure do |config|
1167
+ config.token = "ваш_jwt_токен"
892
1168
 
893
- # Парсинг содержимого
894
- abstract = client.parse_abstract(patent)
895
- description = client.parse_description(patent, format: :text)
1169
+ # Кеширование (включено по умолчанию)
1170
+ config.cache_enabled = true
1171
+ config.cache_ttl = 300 # 5 минут
1172
+ config.cache_max_size = 1000 # Максимум элементов кеша
896
1173
 
897
- puts "Реферат: #{abstract}"
898
- puts "Описание: #{description}"
899
- ```
1174
+ # Логирование
1175
+ config.log_level = :info # :debug, :info, :warn, :error
1176
+ config.log_requests = true # Логировать API запросы
1177
+ config.log_responses = true # Логировать API ответы
900
1178
 
901
- ### Поиск похожих патентов
1179
+ # Настройки соединения
1180
+ config.connection_pool_size = 5
1181
+ config.connection_keep_alive = true
902
1182
 
903
- ```ruby
904
- # Поиск похожих патентов по ID
905
- similar = client.similar_patents_by_id("RU134694U1_20131120", count: 50)
1183
+ # Управление токенами
1184
+ config.token_expires_at = Time.now + 3600
1185
+ config.token_refresh_callback = -> { refresh_token! }
1186
+ end
1187
+ ```
1188
+
1189
+ ### Конфигурация для конкретных окружений
1190
+
1191
+ Gem автоматически настраивается под окружение с разумными значениями по умолчанию:
1192
+
1193
+ #### Окружение разработки
1194
+
1195
+ ```ruby
1196
+ # Оптимизировано для разработки
1197
+ Rospatent.configure do |config|
1198
+ config.environment = "development"
1199
+ config.token = ENV['ROSPATENT_TOKEN']
1200
+ config.log_level = :debug
1201
+ config.log_requests = true
1202
+ config.log_responses = true
1203
+ config.cache_ttl = 60 # Короткий кеш для разработки
1204
+ config.timeout = 10 # Быстрые таймауты для быстрой обратной связи
1205
+ end
1206
+ ```
1207
+
1208
+ #### Окружение Staging
1209
+
1210
+ ```ruby
1211
+ # Оптимизировано для staging
1212
+ Rospatent.configure do |config|
1213
+ config.environment = "staging"
1214
+ config.token = ENV['ROSPATENT_TOKEN']
1215
+ config.log_level = :info
1216
+ config.cache_ttl = 300 # Более длительный кеш для производительности
1217
+ config.timeout = 45 # Более длительные таймауты для надежности
1218
+ config.retry_count = 3 # Больше повторов для устойчивости
1219
+ end
1220
+ ```
1221
+
1222
+ #### Продакшн окружение
1223
+
1224
+ ```ruby
1225
+ # Оптимизировано для продакшна
1226
+ Rospatent.configure do |config|
1227
+ config.environment = "production"
1228
+ config.token = ENV['ROSPATENT_TOKEN']
1229
+ config.log_level = :warn
1230
+ config.cache_ttl = 600 # Более длительный кеш для производительности
1231
+ config.timeout = 60 # Более длительные таймауты для надежности
1232
+ config.retry_count = 5 # Больше повторов для устойчивости
1233
+ end
1234
+ ```
1235
+
1236
+ ### Переменные окружения и интеграция с Rails
1237
+
1238
+ ⚠️ **КРИТИЧНО**: Понимание приоритета переменных окружения необходимо для избежания проблем конфигурации, особенно в Rails приложениях.
1239
+
1240
+ #### Приоритет конфигурации токена
1241
+
1242
+ 1. **Rails credentials**: `Rails.application.credentials.rospatent_token`
1243
+ 2. **Основная переменная окружения**: `ROSPATENT_TOKEN`
1244
+ 3. **Устаревшая переменная окружения**: `ROSPATENT_API_TOKEN`
1245
+
1246
+ ```bash
1247
+ # Рекомендуемый подход
1248
+ export ROSPATENT_TOKEN="your_jwt_token"
1249
+
1250
+ # Поддержка устаревшего формата (все еще работает)
1251
+ export ROSPATENT_API_TOKEN="your_jwt_token"
1252
+ ```
1253
+
1254
+ #### Приоритет конфигурации уровня логирования
1255
+
1256
+ ```ruby
1257
+ # Переменная окружения имеет приоритет над настройками Rails по умолчанию
1258
+ config.log_level = if ENV.key?("ROSPATENT_LOG_LEVEL")
1259
+ ENV["ROSPATENT_LOG_LEVEL"].to_sym
1260
+ else
1261
+ Rails.env.production? ? :warn : :debug
1262
+ end
1263
+ ```
1264
+
1265
+ ⚠️ **Частая проблема**: Установка `ROSPATENT_LOG_LEVEL=debug` в продакшне переопределит логику Rails и приведёт к появлению DEBUG логов в продакшне!
1266
+
1267
+ #### Полный справочник переменных окружения
1268
+
1269
+ ```bash
1270
+ # Основная конфигурация
1271
+ ROSPATENT_TOKEN="your_jwt_token" # Токен аутентификации API
1272
+ ROSPATENT_ENV="production" # Переопределить Rails.env при необходимости
1273
+ ROSPATENT_API_URL="custom_url" # Переопределить URL API по умолчанию
1274
+
1275
+ # Конфигурация логирования
1276
+ ROSPATENT_LOG_LEVEL="warn" # debug, info, warn, error
1277
+ ROSPATENT_LOG_REQUESTS="false" # Логировать API запросы
1278
+ ROSPATENT_LOG_RESPONSES="false" # Логировать API ответы
1279
+
1280
+ # Конфигурация кеша
1281
+ ROSPATENT_CACHE_ENABLED="true" # Включить/отключить кеширование
1282
+ ROSPATENT_CACHE_TTL="300" # TTL кеша в секундах
1283
+ ROSPATENT_CACHE_MAX_SIZE="1000" # Максимальное количество элементов кеша
1284
+
1285
+ # Конфигурация соединения
1286
+ ROSPATENT_TIMEOUT="30" # Таймаут запроса в секундах
1287
+ ROSPATENT_RETRY_COUNT="3" # Количество повторов
1288
+ ROSPATENT_POOL_SIZE="5" # Размер пула соединений
1289
+ ROSPATENT_KEEP_ALIVE="true" # Keep-alive соединения
1290
+
1291
+ # Переопределения для конкретных окружений
1292
+ ROSPATENT_DEV_API_URL="dev_url" # URL API для разработки
1293
+ ROSPATENT_STAGING_API_URL="staging_url" # URL API для staging
1294
+ ```
1295
+
1296
+ #### Лучшие практики для Rails
1297
+
1298
+ 1. **Используйте Rails credentials для токенов**:
1299
+ ```bash
1300
+ rails credentials:edit
1301
+ # Добавьте: rospatent_token: your_jwt_token
1302
+ ```
1303
+
1304
+ 2. **Установите переменные для конкретных окружений**:
1305
+ ```bash
1306
+ # config/environments/production.rb
1307
+ ENV["ROSPATENT_LOG_LEVEL"] ||= "warn"
1308
+ ENV["ROSPATENT_CACHE_ENABLED"] ||= "true"
1309
+ ```
1310
+
1311
+ 3. **Избегайте установки DEBUG уровня в продакшне**:
1312
+ ```bash
1313
+ # ❌ НЕ ДЕЛАЙТЕ ТАК в продакшне
1314
+ export ROSPATENT_LOG_LEVEL=debug
1315
+
1316
+ # ✅ ДЕЛАЙТЕ ТАК
1317
+ export ROSPATENT_LOG_LEVEL=warn
1318
+ ```
1319
+
1320
+ ### Валидация конфигурации
1321
+
1322
+ ```ruby
1323
+ # Валидация текущей конфигурации
1324
+ errors = Rospatent.validate_configuration
1325
+ if errors.any?
1326
+ puts "Ошибки конфигурации:"
1327
+ errors.each { |error| puts " - #{error}" }
1328
+ else
1329
+ puts "Конфигурация действительна ✓"
1330
+ end
1331
+ ```
1332
+
1333
+ ## Основное использование
1334
+
1335
+ ### Поиск патентов
1336
+
1337
+ ```ruby
1338
+ # Простой поиск
1339
+ results = client.search(q: "солнечная батарея")
1340
+
1341
+ # Поиск на естественном языке
1342
+ results = client.search(qn: "конструкция ракетного двигателя")
1343
+
1344
+ # Расширенный поиск с всеми опциями
1345
+ results = client.search(
1346
+ q: "искусственный интеллект AND нейронная сеть",
1347
+ limit: 50,
1348
+ offset: 100,
1349
+ datasets: ["ru_since_1994"],
1350
+ filter: {
1351
+ "classification.ipc_group": { "values": ["G06N"] },
1352
+ "application.filing_date": { "range": { "gte": "20200101" } }
1353
+ },
1354
+ sort: "publication_date:desc", # то же самое, что 'sort: :pub_date'; см. варианты параметров сортировки в Search#validate_sort_parameter
1355
+ group_by: "family:dwpi", # Группировка по семействам: "family:docdb" или "family:dwpi"
1356
+ include_facets: true,
1357
+ pre_tag: "<mark>", # Оба тега должны быть указаны вместе
1358
+ post_tag: "</mark>", # Могут быть строками или массивами
1359
+ highlight: { # Продвинутая настройка подсветки (независимо от тегов)
1360
+ "profiles" => [
1361
+ { "q" => "нейронная сеть", "pre_tag" => "<b>", "post_tag" => "</b>" },
1362
+ "_searchquery_"
1363
+ ]
1364
+ }
1365
+ )
1366
+
1367
+ # Простая подсветка с тегами (оба тега обязательны)
1368
+ results = client.search(
1369
+ q: "ракета",
1370
+ pre_tag: "<mark>",
1371
+ post_tag: "</mark>"
1372
+ )
1373
+
1374
+ # Многоцветная подсветка с массивами
1375
+ results = client.search(
1376
+ q: "космическая ракета",
1377
+ pre_tag: ["<b>", "<i>"], # Циклическая подсветка
1378
+ post_tag: ["</b>", "</i>"] # разными тегами
1379
+ )
1380
+
1381
+ # Продвинутая подсветка с использованием профилей (независимо от pre_tag/post_tag)
1382
+ results = client.search(
1383
+ q: "ракета",
1384
+ highlight: {
1385
+ "profiles" => [
1386
+ { "q" => "космическая", "pre_tag" => "<b>", "post_tag" => "</b>" },
1387
+ "_searchquery_" # Ссылка на параметры подсветки основного поискового запроса
1388
+ ]
1389
+ }
1390
+ )
1391
+
1392
+ # Группировка по семействам патентов (группирует патенты одного изобретения)
1393
+ results = client.search(
1394
+ q: "ракета",
1395
+ group_by: "family:docdb", # Простые семейства патентов DOCDB
1396
+ datasets: ["dwpi"],
1397
+ limit: 10
1398
+ )
1399
+
1400
+ results = client.search(
1401
+ q: "ракета",
1402
+ group_by: "family:dwpi", # Простые семейства патентов DWPI
1403
+ datasets: ["dwpi"],
1404
+ limit: 10
1405
+ )
1406
+
1407
+ # Обработка результатов
1408
+ puts "Найдено: #{results.total} патентов (доступно #{results.available})"
1409
+ puts "Показано: #{results.count}"
1410
+
1411
+ results.hits.each do |hit|
1412
+ puts "ID: #{hit['id']}"
1413
+ puts "Название: #{hit.dig('biblio', 'ru', 'title')}"
1414
+ puts "Дата: #{hit.dig('common', 'publication_date')}"
1415
+ puts "МПК: #{hit.dig('common', 'classification', 'ipc')&.map {|c| c['fullname']}&.join('; ')}"
1416
+ puts "---"
1417
+ end
1418
+ ```
1419
+
1420
+ ### Расширенные параметры фильтрации
1421
+
1422
+ Параметр `filter` поддерживает сложную фильтрацию с автоматической валидацией и преобразованием форматов:
1423
+
1424
+ #### Списочные фильтры (требуют формат `{"values": [...]}`)
1425
+
1426
+ ```ruby
1427
+ # Фильтры по классификации
1428
+ results = client.search(
1429
+ q: "искусственный интеллект",
1430
+ filter: {
1431
+ "classification.ipc_group": { "values": ["G06N", "G06F"] },
1432
+ "classification.cpc_group": { "values": ["G06N3/", "G06N20/"] }
1433
+ }
1434
+ )
1435
+
1436
+ # Фильтры по авторам и патентообладателям
1437
+ results = client.search(
1438
+ q: "изобретение",
1439
+ filter: {
1440
+ "authors": { "values": ["Иванов И.И.", "Петров П.П."] },
1441
+ "patent_holders": { "values": ["ООО Компания"] },
1442
+ "country": { "values": ["RU", "US"] },
1443
+ "kind": { "values": ["A1", "U1"] }
1444
+ }
1445
+ )
1446
+
1447
+ # Фильтры по ID документов
1448
+ results = client.search(
1449
+ q: "устройство",
1450
+ filter: {
1451
+ "ids": { "values": ["RU134694U1_20131120", "RU2358138C1_20090610"] }
1452
+ }
1453
+ )
1454
+ ```
1455
+
1456
+ #### Диапазонные фильтры по датам (требуют формат `{"range": {"operator": "YYYYMMDD"}}`)
1457
+
1458
+ ```ruby
1459
+ # Автоматическое преобразование формата дат
1460
+ results = client.search(
1461
+ q: "инновация",
1462
+ filter: {
1463
+ "date_published": { "range": { "gte": "2020-01-01", "lte": "2023-12-31" } },
1464
+ "application.filing_date": { "range": { "gte": "2019-06-15" } }
1465
+ }
1466
+ )
1467
+
1468
+ # Прямой формат API (YYYYMMDD)
1469
+ results = client.search(
1470
+ q: "технология",
1471
+ filter: {
1472
+ "date_published": { "range": { "gte": "20200101", "lt": "20240101" } }
1473
+ }
1474
+ )
1475
+
1476
+ # Использование объектов Date (автоматически конвертируются)
1477
+ results = client.search(
1478
+ q: "патент",
1479
+ filter: {
1480
+ "application.filing_date": {
1481
+ "range": {
1482
+ "gte": Date.new(2020, 1, 1),
1483
+ "lte": Date.new(2023, 12, 31)
1484
+ }
1485
+ }
1486
+ }
1487
+ )
1488
+ ```
1489
+
1490
+ **Поддерживаемые операторы дат**: `gt`, `gte`, `lt`, `lte`
1491
+
1492
+ **Преобразование формата дат**:
1493
+ - `"2020-01-01"` → `"20200101"`
1494
+ - `Date.new(2020, 1, 1)` → `"20200101"`
1495
+ - `"20200101"` → `"20200101"` (без изменений)
1496
+
1497
+ #### Сложные составные фильтры
1498
+
1499
+ ```ruby
1500
+ # Комплексный пример фильтра
1501
+ results = client.search(
1502
+ q: "машинное обучение",
1503
+ filter: {
1504
+ # Списочные фильтры
1505
+ "classification.ipc_group": { "values": ["G06N", "G06F"] },
1506
+ "country": { "values": ["RU", "US", "CN"] },
1507
+ "kind": { "values": ["A1", "A2"] },
1508
+ "authors": { "values": ["Иванов И.И."] },
1509
+
1510
+ # Диапазонные фильтры по датам
1511
+ "date_published": { "range": { "gte": "2020-01-01", "lte": "2023-12-31" } },
1512
+ "application.filing_date": { "range": { "gte": "2019-01-01" } }
1513
+ },
1514
+ limit: 50
1515
+ )
1516
+ ```
1517
+
1518
+ **Поддерживаемые поля фильтров**:
1519
+
1520
+ *Списочные фильтры (требуют формат `{"values": [...]}`)::*
1521
+ - `authors` - Авторы патентов
1522
+ - `patent_holders` - Патентообладатели/правопреемники
1523
+ - `country` - Коды стран
1524
+ - `kind` - Типы документов
1525
+ - `ids` - Конкретные ID документов
1526
+ - `classification.ipc*` - Коды классификации IPC
1527
+ - `classification.cpc*` - Коды классификации CPC
1528
+
1529
+ *Фильтры по датам (требуют формат `{"range": {"operator": "YYYYMMDD"}}`)::*
1530
+ - `date_published` - Дата публикации
1531
+ - `application.filing_date` - Дата подачи заявки
1532
+
1533
+ **Валидация фильтров**:
1534
+ - ✅ Автоматическая валидация названий полей
1535
+ - ✅ Валидация структуры (списочный vs диапазонный формат)
1536
+ - ✅ Преобразование и валидация формата дат
1537
+ - ✅ Валидация операторов для диапазонов
1538
+ - ✅ Полезные сообщения об ошибках для неверных фильтров
1539
+
1540
+ ```ruby
1541
+ # Эти примеры вызовут ValidationError с конкретными сообщениями:
1542
+ client.search(
1543
+ q: "тест",
1544
+ filter: { "invalid_field": { "values": ["тест"] } }
1545
+ )
1546
+ # Ошибка: "Invalid filter field: invalid_field"
1547
+
1548
+ client.search(
1549
+ q: "тест",
1550
+ filter: { "authors": ["прямой", "массив"] } # Отсутствует обертка {"values": [...]}
1551
+ )
1552
+ # Ошибка: "Filter 'authors' requires format: {\"values\": [...]}"
1553
+
1554
+ client.search(
1555
+ q: "тест",
1556
+ filter: { "date_published": { "range": { "invalid_op": "20200101" } } }
1557
+ )
1558
+ # Ошибка: "Invalid range operator: invalid_op. Supported: gt, gte, lt, lte"
1559
+ ```
1560
+
1561
+ ### Получение документов патентов
1562
+
1563
+ ```ruby
1564
+ # По идентификатору документа
1565
+ patent = client.patent("RU134694U1_20131120")
1566
+
1567
+ # По компонентам идентификатора
1568
+ patent_doc = client.patent_by_components(
1569
+ "RU", # country_code
1570
+ "134694", # number
1571
+ "U1", # doc_type
1572
+ Date.new(2013, 11, 20) # date (String или объект Date)
1573
+ )
1574
+
1575
+ # Доступ к данным патента
1576
+ title = patent_doc.dig('biblio', 'ru', 'title')
1577
+ abstract = patent_doc.dig('abstract', 'ru')
1578
+ inventors = patent_doc.dig('biblio', 'ru', 'inventor')
1579
+ ```
1580
+ ### Парсинг содержимого патента
1581
+
1582
+ Получение чистого текста или структурированного содержимого:
1583
+
1584
+ ```ruby
1585
+ # Парсинг аннотации
1586
+ abstract_text = client.parse_abstract(patent_doc)
1587
+ abstract_html = client.parse_abstract(patent_doc, format: :html)
1588
+ abstract_ru = client.parse_abstract(patent_doc, language: "ru")
1589
+
1590
+ # Парсинг описания
1591
+ description_text = client.parse_description(patent_doc)
1592
+ description_html = client.parse_description(patent_doc, format: :html)
1593
+
1594
+ # Парсинг описания с разбивкой на секции
1595
+ sections = client.parse_description(patent_doc, format: :sections)
1596
+ sections.each do |section|
1597
+ puts "Секция #{section[:number]}: #{section[:content]}"
1598
+ end
1599
+ ```
1600
+
1601
+ ### Поиск похожих патентов
1602
+
1603
+ ```ruby
1604
+ # Поиск похожих патентов по ID
1605
+ similar = client.similar_patents_by_id("RU134694U1_20131120", count: 50)
906
1606
 
907
1607
  # Поиск похожих патентов по описанию текста
908
1608
  similar = client.similar_patents_by_text(
@@ -950,12 +1650,24 @@ cpc_info = client.classification_code("cpc", code: "B63H11/00", lang: "en")
950
1650
  - `"ru"` - Русский
951
1651
  - `"en"` - Английский
952
1652
 
1653
+ ### Список доступных датасетов
1654
+
1655
+ ```ruby
1656
+ datasets = client.datasets_tree
1657
+ datasets.each do |category|
1658
+ puts "Категория: #{category['name_ru']}"
1659
+ category.children.each do |dataset|
1660
+ puts " #{dataset['id']}: #{dataset['name_ru']}"
1661
+ end
1662
+ end
1663
+ ```
1664
+
953
1665
  ### Медиафайлы и документы
954
1666
 
955
1667
  ```ruby
956
1668
  # Скачивание PDF патента
957
1669
  pdf_data = client.patent_media(
958
- "National", # collection_id
1670
+ "National", # collection_id
959
1671
  "RU", # country_code
960
1672
  "U1", # doc_type
961
1673
  "2013/11/20", # pub_date
@@ -970,127 +1682,291 @@ pdf_data = client.patent_media_by_id(
970
1682
  "National",
971
1683
  "document.pdf"
972
1684
  )
973
-
974
- # Получение доступных датасетов
975
- datasets = client.datasets_tree
976
- datasets.each do |category|
977
- puts "Категория: #{category['name_ru']}"
978
- category.children.each do |dataset|
979
- puts " #{dataset['id']}: #{dataset['name_ru']}"
980
- end
981
- end
982
1685
  ```
983
1686
 
984
1687
  ## Расширенные возможности
985
1688
 
986
1689
  ### Пакетные операции
987
1690
 
1691
+ Эффективная обработка множества патентов с параллельными запросами:
1692
+
988
1693
  ```ruby
989
- patent_ids = ["RU134694U1_20131120", "RU2358138C1_20090610"]
1694
+ document_ids = ["RU134694U1_20131120", "RU2358138C1_20090610", "RU2756123C1_20210927"]
990
1695
 
991
- client.batch_patents(patent_ids) do |patent|
992
- puts "Обработка: #{patent['id']}"
993
- # Ваша логика обработки
1696
+ # Обработка патентов пакетами
1697
+ client.batch_patents(document_ids, batch_size: 5) do |patent_doc|
1698
+ if patent_doc[:error]
1699
+ puts "Ошибка для #{patent_doc[:document_id]}: #{patent_doc[:error]}"
1700
+ else
1701
+ puts "Получен патент: #{patent_doc['id']}"
1702
+ # Обработка документа патента
1703
+ end
994
1704
  end
1705
+
1706
+ # Или сбор всех результатов
1707
+ patents = []
1708
+ client.batch_patents(document_ids) { |doc| patents << doc }
995
1709
  ```
996
1710
 
997
1711
  ### Кеширование
998
1712
 
1713
+ Автоматическое интеллектуальное кеширование улучшает производительность:
1714
+
999
1715
  ```ruby
1000
- # Конфигурация кеша
1001
- Rospatent.configure do |config|
1002
- config.cache_enabled = true
1003
- config.cache_ttl = 600 # 10 минут
1004
- config.cache_max_size = 1000 # Максимум элементов
1005
- end
1716
+ # Кеширование автоматическое и прозрачное
1717
+ patent1 = client.patent("RU134694U1_20131120") # API вызов
1718
+ patent2 = client.patent("RU134694U1_20131120") # Кешированный результат
1006
1719
 
1007
- # Статистика кеша
1720
+ # Проверка статистики кеша
1008
1721
  stats = client.statistics
1009
- puts "Попаданий в кеш: #{stats[:cache_stats][:hits]}"
1722
+ puts "Процент попаданий в кеш: #{stats[:cache_stats][:hit_rate_percent]}%"
1723
+ puts "Всего запросов: #{stats[:requests_made]}"
1724
+ puts "Среднее время ответа: #{stats[:average_request_time]}с"
1725
+
1726
+ # Использование общего кеша между клиентами
1727
+ shared_cache = Rospatent.shared_cache
1728
+ client1 = Rospatent.client(cache: shared_cache)
1729
+ client2 = Rospatent.client(cache: shared_cache)
1730
+
1731
+ # Ручное управление кешем
1732
+ shared_cache.clear # Очистить все кешированные данные
1733
+ expired_count = shared_cache.cleanup_expired # Удалить истекшие записи
1734
+ cache_stats = shared_cache.statistics # Получить детальную статистику кеша
1735
+ ```
1736
+
1737
+ ### Настройка логирования
1738
+
1739
+ Настройка детального логирования для мониторинга и отладки:
1740
+
1741
+ ```ruby
1742
+ # Создание собственного логгера
1743
+ logger = Rospatent::Logger.new(
1744
+ output: Rails.logger, # Или любой объект IO
1745
+ level: :info,
1746
+ formatter: :json # :json или :text
1747
+ )
1748
+
1749
+ client = Rospatent.client(logger: logger)
1750
+
1751
+ # Логи включают:
1752
+ # - API запросы/ответы с временными метками
1753
+ # - Операции кеша (попадания/промахи)
1754
+ # - Детали ошибок с контекстом
1755
+ # - Метрики производительности
1756
+
1757
+ # Доступ к общему логгеру
1758
+ shared_logger = Rospatent.shared_logger(level: :debug)
1010
1759
  ```
1011
1760
 
1012
1761
  ### Обработка ошибок
1013
1762
 
1763
+ Комплексная обработка ошибок с конкретными типами ошибок и улучшенным извлечением сообщений об ошибках:
1764
+
1014
1765
  ```ruby
1015
1766
  begin
1016
- results = client.search(q: "поисковый запрос")
1767
+ patent = client.patent("INVALID_ID")
1768
+ rescue Rospatent::Errors::ValidationError => e
1769
+ puts "Неверный ввод: #{e.message}"
1770
+ puts "Ошибки полей: #{e.errors}" if e.errors.any?
1771
+ rescue Rospatent::Errors::NotFoundError => e
1772
+ puts "Патент не найден: #{e.message}"
1773
+ rescue Rospatent::Errors::RateLimitError => e
1774
+ puts "Ограничение скорости. Повторить через: #{e.retry_after} секунд"
1017
1775
  rescue Rospatent::Errors::AuthenticationError => e
1018
1776
  puts "Ошибка аутентификации: #{e.message}"
1019
- rescue Rospatent::Errors::RateLimitError => e
1020
- puts "Превышен лимит запросов: #{e.message}"
1021
1777
  rescue Rospatent::Errors::ApiError => e
1022
- puts "Ошибка API: #{e.message}"
1778
+ puts "Ошибка API (#{e.status_code}): #{e.message}"
1779
+ puts "ID запроса: #{e.request_id}" if e.request_id
1780
+ retry if e.retryable?
1781
+ rescue Rospatent::Errors::ConnectionError => e
1782
+ puts "Ошибка соединения: #{e.message}"
1783
+ puts "Исходная ошибка: #{e.original_error}"
1023
1784
  end
1785
+
1786
+ # Улучшенное извлечение сообщений об ошибках
1787
+ # Клиент автоматически извлекает сообщения об ошибках из различных форматов ответов API:
1788
+ # - {"result": "Сообщение об ошибке"} (формат API Роспатента)
1789
+ # - {"error": "Сообщение об ошибке"} (стандартный формат)
1790
+ # - {"message": "Сообщение об ошибке"} (альтернативный формат)
1791
+ # - {"details": "Детали валидации"} (ошибки валидации)
1024
1792
  ```
1025
1793
 
1026
- ## Настройка окружения
1794
+ ### Валидация входных данных
1027
1795
 
1028
- ### Разработка
1796
+ Все входные данные автоматически валидируются с полезными сообщениями об ошибках:
1029
1797
 
1030
1798
  ```ruby
1031
- # Оптимизировано для разработки
1032
- Rospatent.configure do |config|
1033
- config.environment = "development"
1034
- config.token = ENV['ROSPATENT_DEV_TOKEN']
1035
- config.log_level = :debug
1036
- config.log_requests = true
1037
- config.log_responses = true
1038
- config.cache_ttl = 60 # Короткий кеш для разработки
1039
- config.timeout = 10 # Быстрые таймауты для быстрой обратной связи
1040
- end
1799
+ # Эти примеры вызовут ValidationError с конкретными сообщениями:
1800
+ client.search(limit: 0) # "Limit must be at least 1"
1801
+ client.patent("") # "Document_id cannot be empty"
1802
+ client.similar_patents_by_text("", count: -1) # Множественные ошибки валидации
1803
+
1804
+ # Валидация включает:
1805
+ # - Типы и форматы параметров
1806
+ # - Валидация формата ID патента
1807
+ # - Валидация формата даты
1808
+ # - Валидация значений перечислений
1809
+ # - Валидация обязательных полей
1041
1810
  ```
1042
1811
 
1043
- ### Staging
1812
+ ### Мониторинг производительности
1813
+
1814
+ Отслеживание производительности и статистики использования:
1044
1815
 
1045
1816
  ```ruby
1046
- # Оптимизировано для staging
1047
- Rospatent.configure do |config|
1048
- config.environment = "staging"
1049
- config.token = ENV['ROSPATENT_TOKEN']
1050
- config.log_level = :info
1051
- config.cache_ttl = 300 # Более длительный кеш для производительности
1052
- config.timeout = 45 # Более длительные таймауты для надежности
1053
- config.retry_count = 3 # Больше повторов для устойчивости
1054
- end
1817
+ # Статистика конкретного клиента
1818
+ stats = client.statistics
1819
+ puts "Выполнено запросов: #{stats[:requests_made]}"
1820
+ puts "Общая продолжительность: #{stats[:total_duration_seconds]}с"
1821
+ puts "Среднее время запроса: #{stats[:average_request_time]}с"
1822
+ puts "Процент попаданий в кеш: #{stats[:cache_stats][:hit_rate_percent]}%"
1823
+
1824
+ # Глобальная статистика
1825
+ global_stats = Rospatent.statistics
1826
+ puts "Окружение: #{global_stats[:configuration][:environment]}"
1827
+ puts "Кеш включен: #{global_stats[:configuration][:cache_enabled]}"
1828
+ puts "URL API: #{global_stats[:configuration][:api_url]}"
1829
+ ```
1830
+
1831
+ ## Интеграция с Rails
1832
+
1833
+ ### Генератор
1834
+
1835
+ ```bash
1836
+ $ rails generate rospatent:install
1055
1837
  ```
1056
1838
 
1057
- ### Продакшн
1839
+ Это создает `config/initializers/rospatent.rb`:
1058
1840
 
1059
1841
  ```ruby
1060
- # Оптимизировано для продакшна
1061
1842
  Rospatent.configure do |config|
1062
- config.environment = "production"
1063
- config.token = ENV['ROSPATENT_TOKEN']
1064
- config.log_level = :warn
1065
- config.cache_ttl = 600 # Более длительный кеш для производительности
1066
- config.timeout = 60 # Более длительные таймауты для надежности
1067
- config.retry_count = 5 # Больше повторов для устойчивости
1843
+ # Приоритет токена: Rails credentials > ROSPATENT_TOKEN > ROSPATENT_API_TOKEN
1844
+ config.token = Rails.application.credentials.rospatent_token ||
1845
+ ENV["ROSPATENT_TOKEN"] ||
1846
+ ENV["ROSPATENT_API_TOKEN"]
1847
+
1848
+ # Конфигурация окружения учитывает ROSPATENT_ENV
1849
+ config.environment = ENV.fetch("ROSPATENT_ENV", Rails.env)
1850
+
1851
+ # КРИТИЧНО: Переменные окружения имеют приоритет над настройками Rails
1852
+ # Это предотвращает появление DEBUG логов в продакшне при ROSPATENT_LOG_LEVEL=debug
1853
+ config.log_level = if ENV.key?("ROSPATENT_LOG_LEVEL")
1854
+ ENV["ROSPATENT_LOG_LEVEL"].to_sym
1855
+ else
1856
+ Rails.env.production? ? :warn : :debug
1857
+ end
1858
+
1859
+ config.cache_enabled = Rails.env.production?
1068
1860
  end
1069
1861
  ```
1070
1862
 
1071
- ## Интеграция с Rails
1863
+ ### Использование с логгером Rails
1072
1864
 
1073
1865
  ```ruby
1074
- # config/initializers/rospatent.rb
1866
+ # В config/initializers/rospatent.rb
1075
1867
  Rospatent.configure do |config|
1076
1868
  config.token = Rails.application.credentials.rospatent_token
1077
- config.environment = Rails.env
1078
- config.cache_enabled = Rails.env.production?
1079
- config.log_level = Rails.env.production? ? :warn : :debug
1080
1869
  end
1081
1870
 
1082
- # В контроллере или сервисе
1871
+ # Создание клиента с логгером Rails
1872
+ logger = Rospatent::Logger.new(
1873
+ output: Rails.logger,
1874
+ level: Rails.env.production? ? :warn : :debug,
1875
+ formatter: :text
1876
+ )
1877
+
1878
+ # Использование в контроллерах/сервисах
1083
1879
  class PatentService
1084
1880
  def initialize
1085
- @client = Rospatent.client
1881
+ @client = Rospatent.client(logger: logger)
1086
1882
  end
1087
1883
 
1088
- def search_patents(query, **options)
1089
- @client.search(q: query, **options)
1884
+ def search_patents(query)
1885
+ @client.search(q: query, limit: 20)
1886
+ rescue Rospatent::Errors::ApiError => e
1887
+ Rails.logger.error "Поиск патентов не удался: #{e.message}"
1888
+ raise
1090
1889
  end
1091
1890
  end
1092
1891
  ```
1093
1892
 
1893
+ ## Тестирование
1894
+
1895
+ ### Запуск тестов
1896
+
1897
+ ```bash
1898
+ # Запуск всех тестов
1899
+ $ bundle exec rake test
1900
+
1901
+ # Запуск конкретного тестового файла
1902
+ $ bundle exec ruby -Itest test/unit/client_test.rb
1903
+
1904
+ # Запуск интеграционных тестов (требуется API токен)
1905
+ $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN=ваш_токен bundle exec rake test_integration
1906
+
1907
+ # Запуск с покрытием
1908
+ $ bundle exec rake coverage
1909
+ ```
1910
+
1911
+ ### Настройка тестов
1912
+
1913
+ Для тестирования сбрасывайте и настраивайте в методе setup каждого теста:
1914
+
1915
+ ```ruby
1916
+ # test/test_helper.rb - Базовая настройка для модульных тестов
1917
+ module Minitest
1918
+ class Test
1919
+ def setup
1920
+ Rospatent.reset # Чистое состояние между тестами
1921
+ Rospatent.configure do |config|
1922
+ config.token = ENV.fetch("ROSPATENT_TEST_TOKEN", "test_token")
1923
+ config.environment = "development"
1924
+ config.cache_enabled = false # Отключить кеш для предсказуемых тестов
1925
+ config.log_level = :error # Уменьшить шум тестов
1926
+ end
1927
+ end
1928
+ end
1929
+ end
1930
+
1931
+ # Для интеграционных тестов - стабильная конфигурация, сброс не нужен
1932
+ class IntegrationTest < Minitest::Test
1933
+ def setup
1934
+ skip unless ENV["ROSPATENT_INTEGRATION_TESTS"]
1935
+
1936
+ @token = ENV.fetch("ROSPATENT_TEST_TOKEN", nil)
1937
+ skip "ROSPATENT_TEST_TOKEN not set" unless @token
1938
+
1939
+ # Сброс не нужен - интеграционные тесты используют согласованную конфигурацию
1940
+ Rospatent.configure do |config|
1941
+ config.token = @token
1942
+ config.environment = "development"
1943
+ config.cache_enabled = true
1944
+ config.log_level = :debug
1945
+ end
1946
+ end
1947
+ end
1948
+ ```
1949
+
1950
+ ### Пользовательские проверки (Minitest)
1951
+
1952
+ ```ruby
1953
+ # test/test_helper.rb
1954
+ module Minitest
1955
+ class Test
1956
+ def assert_valid_patent_id(patent_id, message = nil)
1957
+ message ||= "Ожидается #{patent_id} как действительный ID патента (формат: XX12345Y1_YYYYMMDD)"
1958
+ assert patent_id.match?(/^[A-Z]{2}[A-Z0-9]+[A-Z]\d*_\d{8}$/), message
1959
+ end
1960
+ end
1961
+ end
1962
+
1963
+ # Использование в тестах
1964
+ def test_patent_id_validation
1965
+ assert_valid_patent_id("RU134694U1_20131120")
1966
+ assert_valid_patent_id("RU134694A_20131120")
1967
+ end
1968
+ ```
1969
+
1094
1970
  ## Известные ограничения API
1095
1971
 
1096
1972
  Библиотека использует **Faraday** в качестве HTTP-клиента с поддержкой редиректов для всех endpoints:
@@ -1100,6 +1976,7 @@ end
1100
1976
 
1101
1977
  ⚠️ **Незначительные серверные ограничения**:
1102
1978
  - **Поиск похожих патентов по тексту**: Иногда возвращает `503 Service Unavailable` (проблема сервера, не клиентской реализации)
1979
+
1103
1980
  ⚠️ **Неточности документации**:
1104
1981
  - **Поиск похожих патентов**: Массив совпадений в документации назван `hits`, фактическая реализация использует `data`
1105
1982
  - **Перечень датасетов**: Ключ `name` в фактической реализации содержит признак локализации — `name_ru`, `name_en`
@@ -1148,41 +2025,29 @@ Rospatent::Errors::TimeoutError
1148
2025
  Rospatent::Errors::ServiceUnavailableError
1149
2026
  ```
1150
2027
 
1151
- ## Тестирование
2028
+ ## Rake задачи
1152
2029
 
1153
- ### Запуск тестов
2030
+ Полезные задачи для разработки и обслуживания:
1154
2031
 
1155
2032
  ```bash
1156
- # Все тесты
1157
- $ bundle exec rake test
2033
+ # Валидация конфигурации
2034
+ $ bundle exec rake validate
1158
2035
 
1159
- # Конкретный тестовый файл
1160
- $ bundle exec ruby -Itest test/unit/client_test.rb
2036
+ # Управление кешем
2037
+ $ bundle exec rake cache:stats
2038
+ $ bundle exec rake cache:clear
1161
2039
 
1162
- # Интеграционные тесты (требуется API токен)
1163
- $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN=ваш_токен bundle exec rake test_integration
2040
+ # Генерация документации
2041
+ $ bundle exec rake doc
1164
2042
 
1165
- # Запуск с покрытием
1166
- $ bundle exec rake coverage
1167
- ```
2043
+ # Запуск интеграционных тестов
2044
+ $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN='<ваш_jwt_токен>' bundle exec rake test_integration
1168
2045
 
1169
- ### Настройка тестов
2046
+ # Настройка среды разработки
2047
+ $ bundle exec rake setup
1170
2048
 
1171
- ```ruby
1172
- # test/test_helper.rb
1173
- module Minitest
1174
- class Test
1175
- def setup
1176
- Rospatent.reset
1177
- Rospatent.configure do |config|
1178
- config.token = ENV.fetch("ROSPATENT_TEST_TOKEN", "test_token")
1179
- config.environment = "development"
1180
- config.cache_enabled = false
1181
- config.log_level = :error
1182
- end
1183
- end
1184
- end
1185
- end
2049
+ # Проверки перед релизом
2050
+ $ bundle exec rake release_check
1186
2051
  ```
1187
2052
 
1188
2053
  ## Советы по производительности
@@ -1245,6 +2110,62 @@ Rospatent.configure do |config|
1245
2110
  end
1246
2111
  ```
1247
2112
 
2113
+ ## Разработка
2114
+
2115
+ После клонирования репозитория выполните `bin/setup` для установки зависимостей. Затем запустите `rake test` для выполнения тестов.
2116
+
2117
+ ### Настройка разработки
2118
+
2119
+ ```bash
2120
+ $ git clone https://hub.mos.ru/ad/rospatent.git
2121
+ $ cd rospatent
2122
+ $ bundle install
2123
+ $ bundle exec rake setup
2124
+ ```
2125
+
2126
+ ### Запуск тестов
2127
+
2128
+ ```bash
2129
+ # Модульные тесты
2130
+ $ bundle exec rake test
2131
+
2132
+ # Интеграционные тесты (требуется API токен)
2133
+ $ ROSPATENT_INTEGRATION_TESTS=true ROSPATENT_TEST_TOKEN=ваш_токен bundle exec rake test_integration
2134
+
2135
+ # Стиль кода
2136
+ $ bundle exec rubocop
2137
+
2138
+ # Все проверки
2139
+ $ bundle exec rake ci
2140
+ ```
2141
+
2142
+ ### Интерактивная консоль
2143
+
2144
+ ```bash
2145
+ $ bin/console
2146
+ ```
2147
+
2148
+ ## Содействие
2149
+
2150
+ Отчеты об ошибках и pull request приветствуются на MosHub по адресу https://hub.mos.ru/ad/rospatent.
2151
+
2152
+ ### Руководство по разработке
2153
+
2154
+ 1. **Пишите тесты**: Убедитесь, что все новые функции имеют соответствующие тесты
2155
+ 2. **Следуйте стилю**: Выполните `rubocop` и исправьте любые проблемы стиля
2156
+ 3. **Документируйте изменения**: Обновите README и CHANGELOG
2157
+ 4. **Валидируйте конфигурацию**: Запустите `rake validate` перед отправкой
2158
+
2159
+ ### Процесс релиза
2160
+
2161
+ ```bash
2162
+ # Проверки перед релизом
2163
+ $ bundle exec rake release_check
2164
+
2165
+ # Обновление версии и релиз
2166
+ $ bundle exec rake release
2167
+ ```
2168
+
1248
2169
  ---
1249
2170
 
1250
2171
  ## Changelog