vapey 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2affd2122165c29628cc2e83df98c7358557663d4181c53efe919fb5dea789b3
4
+ data.tar.gz: ff27833f4262e1ae17c823669403002a2918744677a12b947c67ddf33cc5bf35
5
+ SHA512:
6
+ metadata.gz: 63df437595c1ce729de60c60451eeb7bb30939edc1dcbd69c3044540be29a2e6d5546f068154c3424b52210af8586df9057dd49908a3e398de0319ec2d5eb7a9
7
+ data.tar.gz: 705230619fc1c896c29ce256da5c6db438e2e12d018affa9c45ea850a385f690999d17137f0789d66b9f8a2da2f22b6cecfca9678cdac71e0b0be2167ad63c14
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 David Giffin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Vapey
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "vapey"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install vapey
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/vapey/rails .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ module Vapey
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,52 @@
1
+ module Vapey
2
+ class VapeyController < ApplicationController
3
+
4
+ def healthcheck
5
+ vapey = Vapey::Search.new
6
+
7
+ begin
8
+ # Send a PING command to Redis
9
+ redis_ping_response = vapey.redis.ping
10
+
11
+ # Check if the Redis server responded with "PONG"
12
+ if redis_ping_response == 'PONG'
13
+
14
+ # create the index if it's not already there
15
+ vapey.create_index
16
+ else
17
+ render json: { status: 'unhealthy', message: 'Redis connection is unhealthy' }
18
+ end
19
+ rescue Redis::CommandError, Redis::CannotConnectError => e
20
+ if e.message == "Index already exists"
21
+ render json: { status: 'healthy', message: 'Redis connection is healthy' }
22
+ else
23
+ render json: { status: 'unhealthy', message: "Redis connection error: #{e.message}" }
24
+ end
25
+ end
26
+ end
27
+
28
+ def search
29
+ vapey = Vapey::Search.new
30
+ result = vapey.find_by_embedding(params[:embedding])
31
+ render json: result
32
+ end
33
+
34
+ def upsert
35
+ vapey = Vapey::Search.new
36
+
37
+ # Hopefully don't have to do this
38
+ ## json_params = JSON.parse(request.raw_post)
39
+
40
+ # Extract the key and value from the request data
41
+ id = params[:id]
42
+
43
+ # Upsert the record into the Redis database
44
+ vapey.redis.json_set("item:#{id}", Rejson::Path.root_path, params.deep_stringify_keys)
45
+
46
+ # Return a success response
47
+ render json: { status: 'success', message: "Record upserted successfully", key: "item:#{id}" }
48
+ end
49
+
50
+
51
+ end
52
+ end
@@ -0,0 +1,6 @@
1
+ module Vapey
2
+ module Rails
3
+ module ApplicationHelper
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Vapey
2
+ module Rails
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module Vapey
2
+ module Rails
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: "from@example.com"
5
+ layout "mailer"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module Vapey
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Vapey rails</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "vapey/rails/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,8 @@
1
+ Vapey::Engine.routes.draw do
2
+
3
+ scope '/', defaults: { format: :json } do
4
+ get '/healthcheck', to: 'vapey#healthcheck'
5
+ post '/search', to: 'vapey#search'
6
+ post '/upsert', to: 'vapey#upsert'
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :vapey_rails do
3
+ # # Task goes here
4
+ # end
data/lib/vapey/core.rb ADDED
@@ -0,0 +1,5 @@
1
+ %w[
2
+ markdown_parser
3
+ reindexer
4
+ uploader
5
+ ].each { |name| require_relative name }
@@ -0,0 +1,7 @@
1
+ require 'rails'
2
+
3
+ module Vapey
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Vapey
6
+ end
7
+ end
@@ -0,0 +1,147 @@
1
+
2
+ class Vapey::MarkdownParser
3
+ attr_reader :sections
4
+
5
+ class << self
6
+
7
+ def redis
8
+ @@redis ||= Redis.new(host: ENV.fetch("REDIS_HOST") { "127.0.0.1" }, port: ENV.fetch("REDIS_PORT") { "6379" }.to_i)
9
+ end
10
+
11
+ def openai
12
+ @@openai ||= OpenAI::Client.new(access_token: ENV.fetch("OPENAPI_ACCESS_TOKEN"))
13
+ end
14
+
15
+ def process(directory)
16
+
17
+ git_file_list(directory).map do |file|
18
+ file_path = File.expand_path(File.join(directory, file))
19
+
20
+ next unless file.match(/\.md$/)
21
+
22
+ puts "processing file: #{file_path}"
23
+
24
+ page_id = Digest::SHA1.hexdigest(file_path)
25
+ title = File.read(file_path).lines.first.strip.gsub(/^#\s+/,'')
26
+ checksum = Digest::MD5.hexdigest(File.read(file_path))
27
+
28
+ parser = Vapey::MarkdownParser.new(file_path)
29
+ sections = parser.sections.map do |section|
30
+ section = process_markdown(section)
31
+ section_title = section.lines.first.strip.gsub(/^i[#]+\s+/,'')
32
+ section_id = Digest::MD5.hexdigest(section)
33
+ id = Digest::MD5.hexdigest("#{page_id}:#{section_id}")
34
+
35
+ # get cached embedding or call out to openai
36
+ token_count, embedding = get_embedding(section, section_id)
37
+
38
+ {
39
+ id: id,
40
+ page_id: page_id,
41
+ section_id: section_id,
42
+ file: file,
43
+ title: title,
44
+ section_title: section_title,
45
+ content: section,
46
+ checksum: checksum,
47
+ token_count: token_count,
48
+ embedding: embedding,
49
+ }
50
+
51
+ end
52
+ end.compact.flatten
53
+ end
54
+
55
+ def get_embedding(section, section_id)
56
+ token_count = nil
57
+ embedding = nil
58
+ data = redis.get(section_id)
59
+
60
+ if data.nil?
61
+
62
+ # OpenAI recommends replacing newlines with spaces for best results (specific to embeddings)
63
+ input = section.gsub(/\n/m, ' ')
64
+ response = openai.embeddings(parameters: { input: input, model: "text-embedding-ada-002"})
65
+
66
+ token_count = response['usage']['total_tokens']
67
+ embedding = response['data'].first['embedding']
68
+
69
+ redis.set(section_id, {'token_count' => token_count, 'embedding' => embedding}.to_json)
70
+ else
71
+ cached_embedding = JSON.parse(data)
72
+ token_count = cached_embedding['token_count']
73
+ embedding = cached_embedding['embedding']
74
+ end
75
+ [token_count, embedding]
76
+ end
77
+
78
+ def git_file_list(dir)
79
+ `cd #{dir} && git ls-files`.split("\n")
80
+ end
81
+
82
+ def process_markdown(file)
83
+ mkdocs_url = "https://docs-mkdocs.releaseapp.io"
84
+
85
+ # handle images
86
+ # TODO: deal with spaces??
87
+ file.gsub!(/(!\[[^\]]*?\])([\(<]+)[\.\/]+\.gitbook\/assets\/(.*?)([\)>]+)/m) do
88
+ "#{$1}#{$2}#{mkdocs_url}/img/#{$3}#{$4}"
89
+ end
90
+
91
+ # remove the .md extension from the end of the URLs from gitbook
92
+ file.gsub!(/(\[[^\]]+?\])\((.*?)\.md([#a-z0-9]*)\)/) do |match|
93
+ "#{$1}(#{$2}#{$3})"
94
+ end
95
+
96
+ # handle "mentions"
97
+ file.gsub!(/\[([^\]]+?).md\]\((.*?)\.md([#a-z0-9]*) "mention"\)/) do |match|
98
+ link = "#{$2}#{$3}"
99
+ title = $1.gsub("-", ' ').titleize
100
+ "[#{title}](#{link})"
101
+ end
102
+
103
+ # convert gitbook hints to admonitions
104
+ # multi-line shortest match ...
105
+ file.gsub!(/{%\s+hint style="(.*?)"\s+?%}(.*?){% endhint %}/m) do |match|
106
+ ret = "!!! #{$1}\n"
107
+ ret += $2.lines.map{|line| " #{line}" }.join()
108
+ ret
109
+ end
110
+
111
+ file
112
+ end
113
+
114
+ end
115
+
116
+ def initialize(file_path)
117
+ @file_path = file_path
118
+ @sections = []
119
+ parse_file
120
+ end
121
+
122
+ def parse_file
123
+ current_section = []
124
+ File.read(@file_path).lines do |line|
125
+ # Check if the line is a header (starts with one or more '#' characters)
126
+ if header?(line)
127
+ # Save the previous section if it's not empty
128
+ @sections << current_section.join unless current_section.empty?
129
+ # Start a new section
130
+ current_section = [line]
131
+ else
132
+ # Add the line to the current section
133
+ current_section << line
134
+ end
135
+ end
136
+ # Save the last section if it's not empty
137
+ @sections << current_section.join unless current_section.empty?
138
+ end
139
+
140
+ private
141
+
142
+ # Check if a line is a Markdown header
143
+ def header?(line)
144
+ line.strip.start_with?('#')
145
+ end
146
+ end
147
+
@@ -0,0 +1,72 @@
1
+ require 'bundler/setup'
2
+ require 'openai'
3
+ require 'dotenv'
4
+ require 'optparse'
5
+ require 'redis'
6
+ require 'rejson'
7
+ require 'active_support/all'
8
+
9
+ require_relative 'markdown_parser'
10
+
11
+ Dotenv.load
12
+
13
+
14
+ class Vapey::Reindexer
15
+ attr_accessor :redis
16
+
17
+ class << self
18
+ def invoke
19
+ options = { drop: false }
20
+ OptionParser.new do |opts|
21
+ opts.banner = "Usage: reindexer.rb [options]"
22
+
23
+ opts.on('-s', '--source SOURCE_DIR', 'Source directory') do |source_dir|
24
+ options[:source_dir] = source_dir
25
+ end
26
+
27
+ opts.on('-d', '--drop', 'Drop and create index before reindexing') do
28
+ options[:drop] = true
29
+ end
30
+
31
+ end.parse!
32
+
33
+ # Validate the presence of source and destination directories
34
+ unless options[:source_dir]
35
+ puts "Error: source directory (-s or --source) must be specified."
36
+ exit 1
37
+ end
38
+
39
+ # Instantiate the ToMkDocs class and perform the conversion
40
+ reindexer = Vapey::Reindexer.new
41
+ reindexer.run(options[:source_dir], drop: options[:drop])
42
+
43
+ puts "Reindexer completed successfully."
44
+ end
45
+ end
46
+
47
+ def initialize
48
+ @redis ||= Redis.new(host: ENV.fetch("REDIS_HOST") { "127.0.0.1" }, port: ENV.fetch("REDIS_PORT") { "6379" }.to_i)
49
+ end
50
+
51
+ def run(directory = nil, drop: false)
52
+ vapey = Vapey::Search.new
53
+
54
+ if drop
55
+ vapey.recreate_index
56
+ end
57
+
58
+ directory ||= ENV.fetch("DOCS_DIRECTORY") { "/Users/david/development/docs/examples" }
59
+ sections = Vapey::MarkdownParser.process(directory)
60
+ sections.each do |section|
61
+ puts "indexing: #{section[:file]} section: #{section[:section_title]}"
62
+ data = section.stringify_keys
63
+ id = data['id']
64
+ vapey.redis.json_set("item:#{id}", Rejson::Path.root_path, data)
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ if __FILE__ == $0
71
+ Vapey::Reindexer::invoke
72
+ end
@@ -0,0 +1,30 @@
1
+ require 'bundler/setup'
2
+ require 'openai'
3
+ require 'httparty'
4
+ require 'dotenv'
5
+
6
+ require_relative 'markdown_parser'
7
+
8
+ Dotenv.load
9
+
10
+
11
+ class Vapey::Uploader
12
+
13
+ class << self
14
+ def invoke(directory = nil)
15
+ # TODO: add command line args / OptionParser
16
+ directory ||= ENV.fetch("DOCS_DIRECTORY") { "/Users/david/development/docs/examples" }
17
+ sections = Vapey::MarkdownParser.process(directory)
18
+ sections.each do |section|
19
+ puts "uploading: #{section[:file]} section: #{section[:section_title]}"
20
+ Uploader::Partay.post('/upsert', { headers: {"Content-Type": "application/json"}, body: section.to_json })
21
+ end
22
+ end
23
+ end
24
+
25
+ class Partay
26
+ include HTTParty
27
+ base_uri ENV.fetch("BASE_URL") { "127.0.0.1:9292" }
28
+ end
29
+
30
+ end
@@ -0,0 +1,3 @@
1
+ module Vapey
2
+ VERSION = "0.1.0"
3
+ end
data/lib/vapey.rb ADDED
@@ -0,0 +1,53 @@
1
+ require "vapey/version"
2
+ require "vapey/engine"
3
+
4
+ module Vapey
5
+ class Search
6
+
7
+ def find_by_embedding(embedding)
8
+ results = redis.call([
9
+ "FT.SEARCH", "index", "@embedding:[VECTOR_RANGE $r $BLOB]=>{$YIELD_DISTANCE_AS: my_scores}",
10
+ "PARAMS", "4", "BLOB", embedding.pack("E*"), "r", "5",
11
+ "LIMIT", "0", "10", "SORTBY", "my_scores", "DIALECT", "2"
12
+ ])
13
+ count = results[0]
14
+
15
+ output = []
16
+ results[1..].each_slice(2) do |key,value|
17
+ data = JSON.parse(value[3])
18
+ result = { key: key }.merge(data)
19
+ result.delete("embedding")
20
+ output << result
21
+ end
22
+
23
+ output
24
+ end
25
+
26
+ def recreate_index
27
+ redis.call(["FT.DROP", "index"])
28
+ create_index
29
+ end
30
+
31
+ def create_index
32
+ schema = {
33
+ id: "TAG",
34
+ page_id: "TAG",
35
+ section_id: "TAG",
36
+ file: "TEXT",
37
+ title: "TEXT",
38
+ content: "TEXT",
39
+ checksum: "TEXT",
40
+ token_count: "NUMERIC",
41
+ embedding: "VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT64",
42
+ }
43
+ preamble = "FT.CREATE index ON JSON PREFIX 1 item: SCHEMA "
44
+ command = (preamble + schema.map{|name,type| "$.#{name} AS #{name} #{type}"}.join(" ")).split(" ")
45
+ redis.call(command)
46
+ end
47
+
48
+ def redis
49
+ @redis ||= Redis.new(host: ENV.fetch("REDIS_HOST") { "127.0.0.1" }, port: ENV.fetch("REDIS_PORT") { "6379" }.to_i)
50
+ end
51
+
52
+ end
53
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vapey
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Giffin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dotenv
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: httparty
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 7.0.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 7.0.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: redis
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rejson-rb
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 1.0.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 1.0.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: ruby-openai
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: vapey uses Redis and OpenAI embeddings to index your documentation
98
+ email:
99
+ - david@giffin.org
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - MIT-LICENSE
105
+ - README.md
106
+ - Rakefile
107
+ - app/assets/config/vapey_rails_manifest.js
108
+ - app/assets/stylesheets/vapey/rails/application.css
109
+ - app/controllers/vapey/application_controller.rb
110
+ - app/controllers/vapey/vapey_controller.rb
111
+ - app/helpers/vapey/rails/application_helper.rb
112
+ - app/jobs/vapey/rails/application_job.rb
113
+ - app/mailers/vapey/rails/application_mailer.rb
114
+ - app/models/vapey/application_record.rb
115
+ - app/views/layouts/vapey/rails/application.html.erb
116
+ - config/routes.rb
117
+ - lib/tasks/vapey/rails_tasks.rake
118
+ - lib/vapey.rb
119
+ - lib/vapey/core.rb
120
+ - lib/vapey/engine.rb
121
+ - lib/vapey/markdown_parser.rb
122
+ - lib/vapey/reindexer.rb
123
+ - lib/vapey/uploader.rb
124
+ - lib/vapey/version.rb
125
+ homepage: https://github.com/releasehub-com/vapey
126
+ licenses:
127
+ - MIT
128
+ metadata:
129
+ homepage_uri: https://github.com/releasehub-com/vapey
130
+ source_code_uri: https://github.com/releasehub-com/vapey
131
+ changelog_uri: https://github.com/releasehub-com/vapey/README.md
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '3.0'
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubygems_version: 3.2.32
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: 'vapey: vector search and indexing of your documentation'
151
+ test_files: []