├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── Rakefile ├── lib ├── ngt.rb └── ngt │ ├── ffi.rb │ ├── index.rb │ ├── optimizer.rb │ ├── utils.rb │ └── version.rb ├── ngt.gemspec ├── test ├── index_test.rb ├── optimizer_test.rb ├── support │ └── example.py └── test_helper.rb └── vendor.yml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: [ubuntu-latest, macos-latest, macos-13] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: ruby/setup-ruby@v1 13 | with: 14 | ruby-version: 3.4 15 | bundler-cache: true 16 | - if: ${{ startsWith(matrix.os, 'macos') }} 17 | run: brew install libomp 18 | - run: bundle exec rake vendor:platform 19 | - run: bundle exec rake test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.lock 10 | /vendor/ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.5.0 (2024-10-23) 2 | 3 | - Updated NGT to 2.2.4 4 | - Dropped support for Ruby < 3.1 5 | 6 | ## 0.4.2 (2023-07-24) 7 | 8 | - Fixed error with `dup` and `clone` 9 | 10 | ## 0.4.1 (2022-12-08) 11 | 12 | - Added support for `:float16` object type 13 | - Fixed error with `object` method 14 | 15 | ## 0.4.0 (2022-06-14) 16 | 17 | - Updated NGT to 1.14.6 18 | - Added ARM shared library for Linux 19 | - Improved ARM detection 20 | - Removed deprecated options 21 | - Dropped support for Ruby < 2.7 22 | 23 | ## 0.3.3 (2021-02-15) 24 | 25 | - Added ARM shared library for Mac 26 | 27 | ## 0.3.2 (2020-12-27) 28 | 29 | - Updated NGT to 1.12.2 30 | 31 | ## 0.3.1 (2020-05-17) 32 | 33 | - Updated NGT to 1.11.5 34 | - Improved error message when OpenMP not found on Mac 35 | 36 | ## 0.3.0 (2020-03-25) 37 | 38 | - Updated NGT to 1.10.0 39 | - Added support for OpenMP on Mac 40 | - Create index in memory if no path specified 41 | - Added `normalized_angle` and `normalized_cosine` 42 | 43 | ## 0.2.4 (2020-03-09) 44 | 45 | - Updated NGT to 1.9.1 46 | - Added support for passing an index to optimizers 47 | - Added `dimensions`, `distance_type`, `edge_size_for_creation`, `edge_size_for_search`, and `object_type` methods 48 | 49 | ## 0.2.3 (2020-03-08) 50 | 51 | - Added `load` method 52 | - Deprecated `create` and passing path to `new` 53 | 54 | ## 0.2.2 (2020-02-11) 55 | 56 | - Fixed `Could not find NGT` error on some Linux platforms 57 | 58 | ## 0.2.1 (2020-02-09) 59 | 60 | - Fixed illegal instruction error on some Linux platforms 61 | 62 | ## 0.2.0 (2020-01-26) 63 | 64 | - Changed to Apache 2.0 license to match NGT 65 | - Added shared libraries 66 | - Added optimizer 67 | - Improved performance of `batch_insert` for Numo 68 | 69 | ## 0.1.1 (2019-10-27) 70 | 71 | - Fixed `unable to resolve type 'uint32_t'` error on Ubuntu 72 | 73 | ## 0.1.0 (2019-10-22) 74 | 75 | - First release 76 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "minitest", ">= 5" 7 | gem "numo-narray", platform: :mri 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016-2020 Yahoo Japan Corporation 2 | Copyright 2019-2021 Andrew Kane 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGT Ruby 2 | 3 | [NGT](https://github.com/yahoojapan/NGT) - high-speed approximate nearest neighbors - for Ruby 4 | 5 | [![Build Status](https://github.com/ankane/ngt-ruby/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/ngt-ruby/actions) 6 | 7 | ## Installation 8 | 9 | Add this line to your application’s Gemfile: 10 | 11 | ```ruby 12 | gem "ngt" 13 | ``` 14 | 15 | On Mac, also install OpenMP: 16 | 17 | ```sh 18 | brew install libomp 19 | ``` 20 | 21 | NGT is not available for Windows 22 | 23 | ## Getting Started 24 | 25 | Prep your data 26 | 27 | ```ruby 28 | objects = [ 29 | [1, 1, 2, 1], 30 | [5, 4, 6, 5], 31 | [1, 2, 1, 2] 32 | ] 33 | ``` 34 | 35 | Create an index 36 | 37 | ```ruby 38 | index = Ngt::Index.new(dimensions) 39 | ``` 40 | 41 | Insert objects 42 | 43 | ```ruby 44 | index.batch_insert(objects) 45 | ``` 46 | 47 | Search the index 48 | 49 | ```ruby 50 | index.search(query, size: 3) 51 | ``` 52 | 53 | Save the index 54 | 55 | ```ruby 56 | index.save(path) 57 | ``` 58 | 59 | Load an index 60 | 61 | ```ruby 62 | index = Ngt::Index.load(path) 63 | ``` 64 | 65 | Get an object by id 66 | 67 | ```ruby 68 | index.object(id) 69 | ``` 70 | 71 | Insert a single object 72 | 73 | ```ruby 74 | index.insert(object) 75 | ``` 76 | 77 | Remove an object by id 78 | 79 | ```ruby 80 | index.remove(id) 81 | ``` 82 | 83 | Build the index 84 | 85 | ```ruby 86 | index.build_index 87 | ``` 88 | 89 | Optimize the index 90 | 91 | ```ruby 92 | optimizer = Ngt::Optimizer.new(outgoing: 10, incoming: 120) 93 | optimizer.adjust_search_coefficients(index) 94 | optimizer.execute(index, new_path) 95 | ``` 96 | 97 | ## Full Example 98 | 99 | ```ruby 100 | dim = 10 101 | objects = [] 102 | 100.times do |i| 103 | objects << dim.times.map { rand(100) } 104 | end 105 | 106 | index = Ngt::Index.new(dim) 107 | index.batch_insert(objects) 108 | 109 | query = objects[0] 110 | result = index.search(query, size: 3) 111 | 112 | result.each do |res| 113 | puts "#{res[:id]}, #{res[:distance]}" 114 | p index.object(res[:id]) 115 | end 116 | ``` 117 | 118 | ## Index Options 119 | 120 | Defaults shown below 121 | 122 | ```ruby 123 | Ngt::Index.new(dimensions, 124 | edge_size_for_creation: 10, 125 | edge_size_for_search: 40, 126 | object_type: :float, # :float, :integer 127 | distance_type: :l2, # :l1, :l2, :hamming, :angle, :cosine, :normalized_angle, :normalized_cosine, :jaccard 128 | path: nil 129 | ) 130 | ``` 131 | 132 | ## Optimizer Options 133 | 134 | Defaults shown below 135 | 136 | ```ruby 137 | Ngt::Optimizer.new( 138 | outgoing: 10, 139 | incoming: 120, 140 | queries: 100, 141 | low_accuracy_from: 0.3, 142 | low_accuracy_to: 0.5, 143 | high_accuracy_from: 0.8, 144 | high_accuracy_to: 0.9, 145 | gt_epsilon: 0.1, 146 | merge: 0.2 147 | ) 148 | ``` 149 | 150 | ## Data 151 | 152 | Data can be an array of arrays 153 | 154 | ```ruby 155 | [[1, 2, 3], [4, 5, 6]] 156 | ``` 157 | 158 | Or a Numo array 159 | 160 | ```ruby 161 | Numo::NArray.cast([[1, 2, 3], [4, 5, 6]]) 162 | ``` 163 | 164 | ## Resources 165 | 166 | - [ANN Benchmarks](https://github.com/erikbern/ann-benchmarks) 167 | 168 | ## Credits 169 | 170 | This library is modeled after NGT’s [Python API](https://github.com/yahoojapan/NGT/blob/master/python/README-ngtpy.md). 171 | 172 | ## History 173 | 174 | View the [changelog](https://github.com/ankane/ngt-ruby/blob/master/CHANGELOG.md) 175 | 176 | ## Contributing 177 | 178 | Everyone is encouraged to help improve this project. Here are a few ways you can help: 179 | 180 | - [Report bugs](https://github.com/ankane/ngt-ruby/issues) 181 | - Fix bugs and [submit pull requests](https://github.com/ankane/ngt-ruby/pulls) 182 | - Write, clarify, or fix documentation 183 | - Suggest or add new features 184 | 185 | To get started with development: 186 | 187 | ```sh 188 | git clone https://github.com/ankane/ngt-ruby.git 189 | cd ngt-ruby 190 | bundle install 191 | bundle exec rake vendor:all 192 | bundle exec rake test 193 | ``` 194 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | task default: :test 5 | Rake::TestTask.new do |t| 6 | t.libs << "test" 7 | t.pattern = "test/**/*_test.rb" 8 | end 9 | 10 | # ensure vendor files exist 11 | task :ensure_vendor do 12 | vendor_config.fetch("platforms").each_key do |k| 13 | raise "Missing directory: #{k}" unless Dir.exist?("vendor/#{k}") 14 | end 15 | end 16 | 17 | Rake::Task["build"].enhance [:ensure_vendor] 18 | 19 | def download_platform(platform) 20 | require "fileutils" 21 | require "open-uri" 22 | require "tmpdir" 23 | 24 | config = vendor_config.fetch("platforms").fetch(platform) 25 | url = config.fetch("url") 26 | sha256 = config.fetch("sha256") 27 | 28 | puts "Downloading #{url}..." 29 | contents = URI.parse(url).read 30 | 31 | computed_sha256 = Digest::SHA256.hexdigest(contents) 32 | raise "Bad hash: #{computed_sha256}" if computed_sha256 != sha256 33 | 34 | file = Tempfile.new(binmode: true) 35 | file.write(contents) 36 | 37 | vendor = File.expand_path("vendor", __dir__) 38 | FileUtils.mkdir_p(vendor) 39 | 40 | dest = File.join(vendor, platform) 41 | FileUtils.rm_r(dest) if Dir.exist?(dest) 42 | 43 | # run apt install unzip on Linux 44 | system "unzip", "-q", file.path, "-d", dest, exception: true 45 | end 46 | 47 | def vendor_config 48 | @vendor_config ||= begin 49 | require "yaml" 50 | YAML.safe_load_file("vendor.yml") 51 | end 52 | end 53 | 54 | namespace :vendor do 55 | task :all do 56 | vendor_config.fetch("platforms").each_key do |k| 57 | download_platform(k) 58 | end 59 | end 60 | 61 | task :platform do 62 | if Gem.win_platform? 63 | download_platform("x64-mingw") 64 | elsif RbConfig::CONFIG["host_os"] =~ /darwin/i 65 | if RbConfig::CONFIG["host_cpu"] =~ /arm|aarch64/i 66 | download_platform("arm64-darwin") 67 | else 68 | download_platform("x86_64-darwin") 69 | end 70 | else 71 | if RbConfig::CONFIG["host_cpu"] =~ /arm|aarch64/i 72 | download_platform("aarch64-linux") 73 | else 74 | download_platform("x86_64-linux") 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/ngt.rb: -------------------------------------------------------------------------------- 1 | # dependencies 2 | require "ffi" 3 | 4 | # stdlib 5 | require "tmpdir" 6 | 7 | # modules 8 | require_relative "ngt/utils" 9 | require_relative "ngt/index" 10 | require_relative "ngt/optimizer" 11 | require_relative "ngt/version" 12 | 13 | module Ngt 14 | class Error < StandardError; end 15 | 16 | class << self 17 | attr_accessor :ffi_lib 18 | end 19 | lib_path = 20 | if Gem.win_platform? 21 | "x64-mingw/ngt.dll" 22 | elsif RbConfig::CONFIG["host_os"] =~ /darwin/i 23 | if RbConfig::CONFIG["host_cpu"] =~ /arm|aarch64/i 24 | "arm64-darwin/libngt.dylib" 25 | else 26 | "x86_64-darwin/libngt.dylib" 27 | end 28 | else 29 | if RbConfig::CONFIG["host_cpu"] =~ /arm|aarch64/i 30 | "aarch64-linux/libngt.so" 31 | else 32 | "x86_64-linux/libngt.so" 33 | end 34 | end 35 | vendor_lib = File.expand_path("../vendor/#{lib_path}", __dir__) 36 | self.ffi_lib = [vendor_lib] 37 | 38 | # friendlier error message 39 | autoload :FFI, "ngt/ffi" 40 | end 41 | -------------------------------------------------------------------------------- /lib/ngt/ffi.rb: -------------------------------------------------------------------------------- 1 | module Ngt 2 | module FFI 3 | extend ::FFI::Library 4 | 5 | begin 6 | ffi_lib Ngt.ffi_lib 7 | rescue LoadError => e 8 | if e.message.include?("Library not loaded: /usr/local/opt/libomp/lib/libomp.dylib") && e.message.include?("Reason: image not found") 9 | raise LoadError, "OpenMP not found. Run `brew install libomp`" 10 | else 11 | raise e 12 | end 13 | end 14 | 15 | # https://github.com/yahoojapan/NGT/blob/master/lib/NGT/Capi.h 16 | # keep same order 17 | 18 | # use uint32 instead of uint32_t 19 | # to prevent "unable to resolve type" error on Ubuntu 20 | 21 | class ObjectDistance < ::FFI::Struct 22 | layout :id, :int, 23 | :distance, :float 24 | end 25 | 26 | enum :distance_type, [:l1, :l2, :hamming, :angle, :cosine, :normalized_angle, :normalized_cosine, :jaccard] 27 | 28 | attach_function :ngt_open_index, %i[string pointer], :pointer 29 | attach_function :ngt_create_graph_and_tree, %i[string pointer pointer], :pointer 30 | attach_function :ngt_create_graph_and_tree_in_memory, %i[pointer pointer], :pointer 31 | attach_function :ngt_create_property, %i[pointer], :pointer 32 | attach_function :ngt_save_index, %i[pointer string pointer], :bool 33 | attach_function :ngt_get_property, %i[pointer pointer pointer], :bool 34 | attach_function :ngt_get_property_dimension, %i[pointer pointer], :int32_t 35 | attach_function :ngt_set_property_dimension, %i[pointer int32_t pointer], :bool 36 | attach_function :ngt_set_property_edge_size_for_creation, %i[pointer int16_t pointer], :bool 37 | attach_function :ngt_set_property_edge_size_for_search, %i[pointer int16_t pointer], :bool 38 | attach_function :ngt_is_property_object_type_float, %i[int32_t], :bool 39 | attach_function :ngt_is_property_object_type_float16, %i[int32_t], :bool 40 | attach_function :ngt_get_property_object_type, %i[pointer pointer], :int32_t 41 | attach_function :ngt_set_property_object_type_float, %i[pointer pointer], :bool 42 | attach_function :ngt_set_property_object_type_float16, %i[pointer pointer], :bool 43 | attach_function :ngt_set_property_object_type_integer, %i[pointer pointer], :bool 44 | attach_function :ngt_set_property_distance_type_l1, %i[pointer pointer], :bool 45 | attach_function :ngt_set_property_distance_type_l2, %i[pointer pointer], :bool 46 | attach_function :ngt_set_property_distance_type_angle, %i[pointer pointer], :bool 47 | attach_function :ngt_set_property_distance_type_hamming, %i[pointer pointer], :bool 48 | attach_function :ngt_set_property_distance_type_jaccard, %i[pointer pointer], :bool 49 | attach_function :ngt_set_property_distance_type_cosine, %i[pointer pointer], :bool 50 | attach_function :ngt_set_property_distance_type_normalized_angle, %i[pointer pointer], :bool 51 | attach_function :ngt_set_property_distance_type_normalized_cosine, %i[pointer pointer], :bool 52 | attach_function :ngt_insert_index, %i[pointer pointer uint32 pointer], :int 53 | attach_function :ngt_insert_index_as_float, %i[pointer pointer uint32 pointer], :int 54 | attach_function :ngt_create_empty_results, %i[pointer], :pointer 55 | attach_function :ngt_search_index, %i[pointer pointer int32 size_t float float pointer pointer], :bool 56 | attach_function :ngt_get_result_size, %i[pointer pointer], :uint32 57 | attach_function :ngt_get_result, %i[pointer uint32 pointer], ObjectDistance.by_value 58 | attach_function :ngt_batch_insert_index, %i[pointer pointer uint32 pointer pointer], :bool 59 | attach_function :ngt_create_index, %i[pointer uint32 pointer], :bool 60 | attach_function :ngt_remove_index, %i[pointer int pointer], :bool 61 | attach_function :ngt_get_object_space, %i[pointer pointer], :pointer 62 | attach_function :ngt_get_object_as_float, %i[pointer int pointer], :pointer 63 | attach_function :ngt_get_object_as_integer, %i[pointer int pointer], :pointer 64 | attach_function :ngt_destroy_results, %i[pointer], :void 65 | attach_function :ngt_destroy_property, %i[pointer], :void 66 | attach_function :ngt_close_index, %i[pointer], :void 67 | attach_function :ngt_get_property_edge_size_for_creation, %i[pointer pointer], :int16 68 | attach_function :ngt_get_property_edge_size_for_search, %i[pointer pointer], :int16 69 | attach_function :ngt_get_property_distance_type, %i[pointer pointer], :distance_type 70 | attach_function :ngt_create_error_object, %i[], :pointer 71 | attach_function :ngt_get_error_string, %i[pointer], :string 72 | attach_function :ngt_clear_error_string, %i[pointer], :void 73 | attach_function :ngt_destroy_error_object, %i[pointer], :void 74 | attach_function :ngt_create_optimizer, %i[bool pointer], :pointer 75 | attach_function :ngt_optimizer_adjust_search_coefficients, %i[pointer string pointer], :bool 76 | attach_function :ngt_optimizer_execute, %i[pointer string string pointer], :bool 77 | attach_function :ngt_optimizer_set, %i[pointer int int int float float float float double double pointer], :bool 78 | attach_function :ngt_destroy_optimizer, %i[pointer], :void 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/ngt/index.rb: -------------------------------------------------------------------------------- 1 | module Ngt 2 | class Index 3 | include Utils 4 | 5 | attr_reader :path 6 | 7 | def initialize(index, path) 8 | @index = index 9 | @path = path 10 | 11 | @error = FFI.ngt_create_error_object 12 | @error = ::FFI::AutoPointer.new(@error, FFI.method(:ngt_destroy_error_object)) 13 | 14 | @property = ffi(:ngt_create_property) 15 | @property = ::FFI::AutoPointer.new(@property, FFI.method(:ngt_destroy_property)) 16 | ffi(:ngt_get_property, @index, @property) 17 | end 18 | 19 | def dimensions 20 | @dimensions ||= ffi(:ngt_get_property_dimension, @property) 21 | end 22 | 23 | def distance_type 24 | @distance_type ||= ffi(:ngt_get_property_distance_type, @property) 25 | end 26 | 27 | def edge_size_for_creation 28 | @edge_size_for_creation ||= ffi(:ngt_get_property_edge_size_for_creation, @property) 29 | end 30 | 31 | def edge_size_for_search 32 | @edge_size_for_search ||= ffi(:ngt_get_property_edge_size_for_search, @property) 33 | end 34 | 35 | def object_type 36 | @object_type ||= begin 37 | object_type = ffi(:ngt_get_property_object_type, @property) 38 | if FFI.ngt_is_property_object_type_float(object_type) 39 | :float 40 | elsif FFI.ngt_is_property_object_type_float16(object_type) 41 | :float16 42 | else 43 | :integer 44 | end 45 | end 46 | end 47 | 48 | def insert(object) 49 | object = object.to_a 50 | ffi(:ngt_insert_index, @index, c_object(object), object.size) 51 | end 52 | 53 | def batch_insert(objects, num_threads: 8) 54 | if narray?(objects) 55 | check_dimensions(objects.shape[1]) 56 | 57 | objects = objects.cast_to(Numo::SFloat) unless objects.is_a?(Numo::SFloat) 58 | count = objects.shape[0] 59 | obj = ::FFI::MemoryPointer.new(:char, objects.byte_size) 60 | obj.write_bytes(objects.to_binary) 61 | else 62 | objects = objects.to_a 63 | objects.each do |object| 64 | check_dimensions(object.size) 65 | end 66 | count = objects.size 67 | flat_objects = objects.flatten 68 | obj = ::FFI::MemoryPointer.new(:float, flat_objects.size) 69 | obj.write_array_of_float(flat_objects) 70 | end 71 | 72 | ids = ::FFI::MemoryPointer.new(:uint32, count) 73 | ffi(:ngt_batch_insert_index, @index, obj, count, ids) 74 | 75 | build_index(num_threads: num_threads) 76 | 77 | ids.read_array_of_uint32(count) 78 | end 79 | 80 | def build_index(num_threads: 8) 81 | ffi(:ngt_create_index, @index, num_threads) 82 | end 83 | 84 | def object(id) 85 | if object_type == :float 86 | res = ffi(:ngt_get_object_as_float, object_space, id) 87 | res.read_array_of_float(dimensions) 88 | elsif object_type == :integer 89 | res = ffi(:ngt_get_object_as_integer, object_space, id) 90 | res.read_array_of_uint8(dimensions) 91 | else 92 | raise Error, "Method not supported for this object type" 93 | end 94 | end 95 | 96 | def remove(id) 97 | ffi(:ngt_remove_index, @index, id) 98 | end 99 | 100 | def search(query, size: 20, epsilon: 0.1, radius: nil) 101 | radius ||= -1.0 102 | results = ffi(:ngt_create_empty_results) 103 | query = query.to_a 104 | ffi(:ngt_search_index, @index, c_object(query), query.size, size, epsilon, radius, results) 105 | result_size = ffi(:ngt_get_result_size, results) 106 | ret = [] 107 | result_size.times do |i| 108 | res = ffi(:ngt_get_result, results, i) 109 | ret << { 110 | id: res[:id], 111 | distance: res[:distance] 112 | } 113 | end 114 | ret 115 | ensure 116 | FFI.ngt_destroy_results(results) if results 117 | end 118 | 119 | def save(path = nil) 120 | @path = path || @path || Dir.mktmpdir 121 | ffi(:ngt_save_index, @index, @path) 122 | end 123 | 124 | def close 125 | FFI.ngt_close_index(@index) 126 | end 127 | 128 | def self.new( 129 | dimensions, 130 | path: nil, 131 | edge_size_for_creation: 10, 132 | edge_size_for_search: 40, 133 | object_type: :float, 134 | distance_type: :l2 135 | ) 136 | error = FFI.ngt_create_error_object 137 | 138 | if path && dimensions.nil? 139 | index = ffi(:ngt_open_index, path, error) 140 | else 141 | property = ffi(:ngt_create_property, error) 142 | ffi(:ngt_set_property_dimension, property, dimensions, error) 143 | ffi(:ngt_set_property_edge_size_for_creation, property, edge_size_for_creation, error) 144 | ffi(:ngt_set_property_edge_size_for_search, property, edge_size_for_search, error) 145 | 146 | case object_type.to_s.downcase 147 | when "float" 148 | ffi(:ngt_set_property_object_type_float, property, error) 149 | when "float16" 150 | ffi(:ngt_set_property_object_type_float16, property, error) 151 | when "integer" 152 | ffi(:ngt_set_property_object_type_integer, property, error) 153 | else 154 | raise ArgumentError, "Unknown object type: #{object_type}" 155 | end 156 | 157 | case distance_type.to_s.downcase 158 | when "l1" 159 | ffi(:ngt_set_property_distance_type_l1, property, error) 160 | when "l2" 161 | ffi(:ngt_set_property_distance_type_l2, property, error) 162 | when "angle" 163 | ffi(:ngt_set_property_distance_type_angle, property, error) 164 | when "hamming" 165 | ffi(:ngt_set_property_distance_type_hamming, property, error) 166 | when "jaccard" 167 | ffi(:ngt_set_property_distance_type_jaccard, property, error) 168 | when "cosine" 169 | ffi(:ngt_set_property_distance_type_cosine, property, error) 170 | when "normalized_angle" 171 | ffi(:ngt_set_property_distance_type_normalized_angle, property, error) 172 | when "normalized_cosine" 173 | ffi(:ngt_set_property_distance_type_normalized_cosine, property, error) 174 | else 175 | raise ArgumentError, "Unknown distance type: #{distance_type}" 176 | end 177 | 178 | index = 179 | if path 180 | ffi(:ngt_create_graph_and_tree, path, property, error) 181 | else 182 | ffi(:ngt_create_graph_and_tree_in_memory, property, error) 183 | end 184 | end 185 | 186 | index = ::FFI::AutoPointer.new(index, FFI.method(:ngt_close_index)) 187 | 188 | super(index, path) 189 | ensure 190 | FFI.ngt_destroy_error_object(error) if error 191 | FFI.ngt_destroy_property(property) if property 192 | end 193 | 194 | def self.load(path) 195 | new(nil, path: path) 196 | end 197 | 198 | def self.create(path, dimensions, **options) 199 | warn "[ngt] create is deprecated - use new instead" 200 | new(dimensions, path: path, **options) 201 | end 202 | 203 | # private 204 | def self.ffi(*args) 205 | Utils.ffi(*args) 206 | end 207 | 208 | private 209 | 210 | def narray?(data) 211 | defined?(Numo::NArray) && data.is_a?(Numo::NArray) 212 | end 213 | 214 | def c_object(object) 215 | check_dimensions(object.size) 216 | c_object = ::FFI::MemoryPointer.new(:double, object.size) 217 | c_object.write_array_of_double(object) 218 | c_object 219 | end 220 | 221 | def check_dimensions(d) 222 | raise ArgumentError, "Bad dimensions" if d != dimensions 223 | end 224 | 225 | def object_space 226 | @object_space ||= ffi(:ngt_get_object_space, @index) 227 | end 228 | end 229 | end 230 | -------------------------------------------------------------------------------- /lib/ngt/optimizer.rb: -------------------------------------------------------------------------------- 1 | module Ngt 2 | class Optimizer 3 | include Utils 4 | 5 | def initialize(outgoing: 10, incoming: 120, queries: 100, low_accuracy_from: 0.3, low_accuracy_to: 0.5, high_accuracy_from: 0.8, high_accuracy_to: 0.9, gt_epsilon: 0.1, merge: 0.2) 6 | @error = FFI.ngt_create_error_object 7 | @error = ::FFI::AutoPointer.new(@error, FFI.method(:ngt_destroy_error_object)) 8 | 9 | @optimizer = ffi(:ngt_create_optimizer, true) 10 | @optimizer = ::FFI::AutoPointer.new(@optimizer, FFI.method(:ngt_destroy_optimizer)) 11 | 12 | ffi(:ngt_optimizer_set, @optimizer, outgoing, incoming, queries, low_accuracy_from, 13 | low_accuracy_to, high_accuracy_from, high_accuracy_to, gt_epsilon, merge) 14 | end 15 | 16 | def execute(in_index_path, out_index_path) 17 | ffi(:ngt_optimizer_execute, @optimizer, path(in_index_path), out_index_path) 18 | end 19 | 20 | def adjust_search_coefficients(index_path) 21 | ffi(:ngt_optimizer_adjust_search_coefficients, @optimizer, path(index_path)) 22 | end 23 | 24 | private 25 | 26 | def path(obj) 27 | if obj.is_a?(Ngt::Index) 28 | raise ArgumentError, "Index not saved" unless obj.path 29 | obj.path 30 | else 31 | obj 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/ngt/utils.rb: -------------------------------------------------------------------------------- 1 | module Ngt 2 | module Utils 3 | # private 4 | def self.ffi(method, *args) 5 | res = FFI.send(method, *args) 6 | message = FFI.ngt_get_error_string(args.last) 7 | unless message.empty? 8 | FFI.ngt_clear_error_string(args.last) 9 | raise Error, message 10 | end 11 | res 12 | end 13 | 14 | private 15 | 16 | def ffi(*args) 17 | Utils.ffi(*args, @error) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/ngt/version.rb: -------------------------------------------------------------------------------- 1 | module Ngt 2 | VERSION = "0.5.0" 3 | end 4 | -------------------------------------------------------------------------------- /ngt.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/ngt/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "ngt" 5 | spec.version = Ngt::VERSION 6 | spec.summary = "High-speed approximate nearest neighbors for Ruby" 7 | spec.homepage = "https://github.com/ankane/ngt-ruby" 8 | spec.license = "Apache-2.0" 9 | 10 | spec.author = "Andrew Kane" 11 | spec.email = "andrew@ankane.org" 12 | 13 | spec.files = Dir["*.{md,txt}", "{lib,vendor}/**/*"] 14 | spec.require_path = "lib" 15 | 16 | spec.required_ruby_version = ">= 3.1" 17 | 18 | spec.add_dependency "ffi" 19 | end 20 | -------------------------------------------------------------------------------- /test/index_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class IndexTest < Minitest::Test 4 | def test_works 5 | objects = [ 6 | [1, 1, 2, 1], 7 | [5, 4, 6, 5], 8 | [1, 2, 1, 2] 9 | ] 10 | 11 | index = Ngt::Index.new(4) 12 | assert_equal :l2, index.distance_type 13 | assert_equal 10, index.edge_size_for_creation 14 | assert_equal 40, index.edge_size_for_search 15 | assert_equal :float, index.object_type 16 | 17 | assert_equal [1, 2, 3], index.batch_insert(objects) 18 | path = Dir.mktmpdir 19 | assert_equal true, index.save(path) 20 | 21 | query = objects[0] 22 | result = index.search(query, size: 3) 23 | 24 | assert_equal 3, result.size 25 | assert_equal [1, 3, 2], result.map { |r| r[:id] } 26 | assert_equal 0, result[0][:distance] 27 | assert_in_delta 1.732050776481628, result[1][:distance] 28 | assert_in_delta 7.549834251403809, result[2][:distance] 29 | 30 | index = Ngt::Index.load(path) 31 | assert_equal 4, index.dimensions 32 | result = index.search(query, size: 3) 33 | assert_equal [1, 3, 2], result.map { |r| r[:id] } 34 | end 35 | 36 | def test_zero_vector 37 | objects = [ 38 | [1, 1, 2, 1], 39 | [0, 0, 0, 0], 40 | [1, 2, 1, 2] 41 | ] 42 | 43 | index = Ngt::Index.new(4, distance_type: :cosine) 44 | index.batch_insert(objects) 45 | result = index.search(objects[0], size: 3) 46 | # TODO decide how to handle 47 | assert_equal [1], result.map { |r| r[:id] } 48 | end 49 | 50 | def test_nan 51 | objects = [ 52 | [1, 1, 2, 1], 53 | [Float::NAN, 1, 2, 3], 54 | [1, 2, 1, 2] 55 | ] 56 | 57 | index = Ngt::Index.new(4, distance_type: :cosine) 58 | index.batch_insert(objects) 59 | result = index.search(objects[0], size: 3) 60 | # TODO decide how to handle 61 | assert_equal [1], result.map { |r| r[:id] } 62 | end 63 | 64 | def test_infinite 65 | objects = [ 66 | [1, 1, 2, 1], 67 | [Float::INFINITY, 1, 2, 3], 68 | [1, 2, 1, 2] 69 | ] 70 | 71 | index = Ngt::Index.new(4, distance_type: :cosine) 72 | index.batch_insert(objects) 73 | result = index.search(objects[0], size: 3) 74 | # TODO decide how to handle 75 | assert_equal [1], result.map { |r| r[:id] } 76 | end 77 | 78 | def test_numo 79 | skip if ["jruby", "truffleruby"].include?(RUBY_ENGINE) 80 | 81 | objects = [ 82 | [1, 1, 2, 1], 83 | [5, 4, 6, 5], 84 | [1, 2, 1, 2] 85 | ] 86 | objects = Numo::DFloat.cast(objects) 87 | 88 | index = Ngt::Index.new(4) 89 | assert_equal [1, 2, 3], index.batch_insert(objects) 90 | assert_equal true, index.save 91 | 92 | query = objects[0, true] 93 | result = index.search(query, size: 3) 94 | 95 | assert_equal 3, result.size 96 | assert_equal [1, 3, 2], result.map { |r| r[:id] } 97 | assert_equal 0, result[0][:distance] 98 | assert_in_delta 1.732050776481628, result[1][:distance] 99 | assert_in_delta 7.549834251403809, result[2][:distance] 100 | end 101 | 102 | def test_remove 103 | objects = [ 104 | [1, 1, 2, 1], 105 | [5, 4, 6, 5], 106 | [1, 2, 1, 2] 107 | ] 108 | 109 | index = Ngt::Index.new(4) 110 | assert_equal [1, 2, 3], index.batch_insert(objects) 111 | 112 | assert_equal true, index.remove(3) 113 | # TODO remove assert_raises in 0.5.0 114 | assert_raises do 115 | assert_equal false, index.remove(3) 116 | end 117 | assert_raises do 118 | assert_equal false, index.remove(4) 119 | end 120 | 121 | result = index.search(objects[0]) 122 | assert_equal 2, result.size 123 | end 124 | 125 | def test_object_type_float16 126 | object = [1.5, 2.5, 3.5] 127 | index = Ngt::Index.new(3, object_type: :float16) 128 | assert_equal :float16, index.object_type 129 | assert_equal 1, index.insert(object) 130 | assert_equal true, index.build_index 131 | error = assert_raises(Ngt::Error) do 132 | index.object(1) 133 | end 134 | assert_equal "Method not supported for this object type", error.message 135 | end 136 | 137 | def test_object_type_integer 138 | object = [1, 2, 3] 139 | index = Ngt::Index.new(3, object_type: :integer) 140 | assert_equal :integer, index.object_type 141 | assert_equal 1, index.insert(object) 142 | assert_equal true, index.build_index 143 | assert_equal object, index.object(1) 144 | end 145 | 146 | def test_empty 147 | index = Ngt::Index.new(3) 148 | assert_empty index.batch_insert([]) 149 | end 150 | 151 | def test_ffi_error 152 | error = assert_raises(Ngt::Error) do 153 | Ngt::Index.new(0, path: Dir.mktmpdir) 154 | end 155 | assert_match "Dimension is not specified", error.message 156 | end 157 | 158 | def test_bad_object_type 159 | error = assert_raises(ArgumentError) do 160 | Ngt::Index.new(3, object_type: "bad") 161 | end 162 | assert_equal "Unknown object type: bad", error.message 163 | end 164 | 165 | def test_bad_distance_type 166 | error = assert_raises(ArgumentError) do 167 | Ngt::Index.new(3, distance_type: "bad") 168 | end 169 | assert_equal "Unknown distance type: bad", error.message 170 | end 171 | 172 | def test_insert_bad_dimensions 173 | error = assert_raises(ArgumentError) do 174 | Ngt::Index.new(3).insert([1, 2]) 175 | end 176 | assert_equal "Bad dimensions", error.message 177 | end 178 | 179 | def test_batch_insert_bad_dimensions 180 | error = assert_raises(ArgumentError) do 181 | Ngt::Index.new(3).batch_insert([[1, 2]]) 182 | end 183 | assert_equal "Bad dimensions", error.message 184 | end 185 | 186 | def test_search_bad_dimensions 187 | index = Ngt::Index.new(3) 188 | index.insert([1, 2, 3]) 189 | error = assert_raises(ArgumentError) do 190 | index.search([1, 2]) 191 | end 192 | assert_equal "Bad dimensions", error.message 193 | end 194 | 195 | def test_copy 196 | index = Ngt::Index.new(4) 197 | index.dup 198 | index.clone 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /test/optimizer_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "test_helper" 2 | 3 | class OptimizerTest < Minitest::Test 4 | def test_works 5 | dim = 4 6 | objects = [ 7 | [1, 1, 2, 1], 8 | [5, 4, 6, 5], 9 | [1, 2, 1, 2] 10 | ] 11 | 12 | index = Ngt::Index.new(dim) 13 | index.batch_insert(objects) 14 | index.save 15 | 16 | optimizer = Ngt::Optimizer.new(queries: 1) 17 | optimizer.adjust_search_coefficients(index) 18 | optimizer.execute(index, File.join(Dir.mktmpdir, "index")) 19 | end 20 | 21 | def test_copy 22 | optimizer = Ngt::Optimizer.new(queries: 1) 23 | optimizer.dup 24 | optimizer.clone 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/support/example.py: -------------------------------------------------------------------------------- 1 | from ngt import base as ngt 2 | 3 | objects = [ 4 | [1, 1, 2, 1], 5 | [5, 4, 6, 5], 6 | [1, 2, 1, 2] 7 | ] 8 | 9 | index = ngt.Index.create(b"/tmp/index", 4) 10 | index.insert(objects) 11 | index.save() 12 | 13 | query = objects[0] 14 | result = index.search(query, 3) 15 | 16 | for res in result: 17 | print(str(res.id) + ", " + str(res.distance)) 18 | print(index.get_object(res.id)) 19 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | Bundler.require(:default) 3 | require "minitest/autorun" 4 | require "minitest/pride" 5 | 6 | class Minitest::Test 7 | def setup 8 | # autoload before GC.stress 9 | Ngt::FFI if stress? 10 | 11 | GC.stress = true if stress? 12 | end 13 | 14 | def teardown 15 | GC.stress = false if stress? 16 | end 17 | 18 | def stress? 19 | ENV["STRESS"] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /vendor.yml: -------------------------------------------------------------------------------- 1 | platforms: 2 | x86_64-linux: 3 | url: https://github.com/ankane/ml-builds/releases/download/ngt-2.2.4/ngt-2.2.4-x86_64-linux.zip 4 | sha256: f670bb79bba222679da90d9247ae4ec5d4c40654af52d0a6cdc8b57f71315ccd 5 | aarch64-linux: 6 | url: https://github.com/ankane/ml-builds/releases/download/ngt-2.2.4/ngt-2.2.4-aarch64-linux.zip 7 | sha256: 25e0462ffbaf77a349571a2b5fb5eabf30c383c0edc38cd7dda96e590a0a1e1b 8 | x86_64-darwin: 9 | url: https://github.com/ankane/ml-builds/releases/download/ngt-2.2.4/ngt-2.2.4-x86_64-darwin.zip 10 | sha256: 1537aacc16d6ba39567713f1e875817c76293175e8f084f67e476ca311a48c2c 11 | arm64-darwin: 12 | url: https://github.com/ankane/ml-builds/releases/download/ngt-2.2.4/ngt-2.2.4-aarch64-darwin.zip 13 | sha256: 3cb717bca24d8d1707952595b7daa7c1259022eb060dfe5ded02a6ed0cc93cc5 14 | --------------------------------------------------------------------------------