オントロジーのグラフデータベース化


## はじめに

SUMO(Suggested Upper Merged Ontology) を neo4j に変換する。

![Kobito.A79jKQ.png](https://qiita-image-store.s3.amazonaws.com/0/109010/82fb5fe9-b49a-3d58-78a8-0dcf100af9d9.png "Kobito.A79jKQ.png")


## SUMO

クラス・インスタンス階層や一階述語論理の機構を備えたオントロジー。kifという言語で記述されている。

	(subclass Physical Entity)
	(partition Physical Object Process)

以下のページの Download から zip ファイルでダウンロードする。

http://www.adampease.org/OP/

## 使用するgem

Rubyからneo4jを扱うライブラリとしてneographerやneo4jがあるが、neographerがいまいちよく分からなかったのでneo4jを使う。kifファイルをパースするためにsxpを使う。zipファイルを読むためにrubyzipを使う。

	sudo gem install neo4j
	sudo gem install sxp
	sudo gem install rubyzip

## kifファイルを読む

とりあえずkifファイルを読むモジュールを書く。
kifはS式の一種なので、S式の文字列を読んで配列にしてくれるsxpを使用する。

	SXP.read '((subclass Cat Animal) (instance Tama Cat))'
	=> [[:subclass, :Cat, :Animal], [:instance, :Tama, :Cat]]

コメント行の削除などの掃除をしてから、SXPに食わせる。

	require 'sxp'
	
	module KIF
		module_function
		def parse(file)
			doc = file.read.force_encoding('UTF-8')
			# BOM削除
			doc = doc[1..-1] if doc[0].ord == 65279
			# コメント削除
			_doc = ''
			comment = false
			quote = false
			doc.split(/(\\"|"|;|\n)/).each do |item|
				if comment
					comment = false if item == "\n"
				elsif quote
					quote = false if item == '"'
					_doc += item
				else
					comment = true if item == ';'
					quote = true if item == '"'
					_doc += ' ' if item == '"'  # ComputerInput.kif のバグ対策
					_doc += item unless comment
				end
			end
			# S式
			doc = '(' + _doc + ')'
			sxp = SXP.read(doc)
			return sxp
		end
	end
	
## zipファイルから読み込む

zipファイルを展開してから1ファイルずつ処理してもいいのだが、かさ張るのでzipファイルのまま読むことにする。

	require 'zip'
	
	class KIF::Archive
		def initialize(path)
			@path = path
		end
		def each
			Zip::File.open(@path) do |zip|
				zip.glob('sumo-master/*.kif').each do |entry|
					yield entry
				end
			end
		end
	end

kifファイルのうち、WorldAirports.kif などは項目数は多いがほとんど使わないので除外する。

	sumo = KIF::Archive.new('sumo-master.zip')
	sumo.each do |entry|
		next if entry.name =~ /WorldAirports/
		sxp = entry.get_input_stream{|f| KIF.parse(f) }
	end

## neo4j に書き込む

neo4jに書き込むための writer クラスを定義。

	require 'neo4j-core'

	class Writer
		def create_index
			Neo4j::Label.create('class').create_index(:name)
			Neo4j::Label.create('instance').create_index(:name)
		end
		def write(sxp)
			@nodes ||= Hash.new
			sxp.each do |axiom|
				next if axiom.any?{|item| item.is_a? Array }
				predicate, *arguments = *axiom
				case predicate
				when :subclass
					target, source = *arguments
					source_label = 'class'
					target_label = 'class'
				when :instance
					target, source = *arguments
					source_label = 'class'
					target_label = 'instance'
				when :subrelation, :subAttribute
					target, source = *arguments
					source_label = 'instance'
					target_label = 'instance'
				when :domain, :domainSubclass
					source, order, target = *arguments
					source_label = 'instance'
					target_label = 'class'
				when :range, :rangeSubclass
					source, target = *arguments
					source_label = 'instance'
					target_label = 'class'
				end
				next unless source
				@nodes[source] ||= Neo4j::Node.create({name: source}, source_label)
				@nodes[target] ||= Neo4j::Node.create({name: target}, target_label)
				relation = @nodes[source].create_rel(predicate, @nodes[target])
				relation[:order] = order if order
			end
		end
	end

create_indexはノードのname属性をインデックスにする設定のためのメソッド。writeがS式を受け取ってデータベースに書き込むメソッド。ここでは、kifから以下の式のみを取り出してデータベースに書き込んでいる。

	(subclass target source)
	(instance target source)
	(subrelation target source)
	(subAttribute target source)
	(domain source order target)
	(domainSubclass source order target)
	(range source target)
	(rangeSubclass source target)

domainというのは定義域のことで、要するに項の型を指定している。

	(domain instance 1 Entity)
	(domain instance 2 Class)

rangeというのは値域のことで、関数タイプの述語に対して返り値の型を指定する。

	(range whereFn Region)

グラフデータベースは要素と要素を関係で結ぶので、基本的に(関係 要素 要素)という2項述語しか表現できない。domainとdomainSubclassは3項述語だが、order項をリレーションの属性とすることで表現している。

## 実行

WorldAirports.kif などは項目数ばかり多くてほとんど使わないので除外し、他のものをデータベースに書き込む。

	sumo = KIF::Archive.new('sumo-master.zip')
	session = Neo4j::Session.open(:server_db, 'http://localhost:7474', basic_auth: {username: 'myname', password: 'mypassword'})
	writer = Writer.new
	writer.create_index
	sumo.each do |entry|
		STDERR.puts entry.name
		next unless entry.name !~ /WorldAirports/
		sxp = entry.get_input_stream{|f| KIF.parse(f) }
		writer.write sxp
	end

## ToDo

WordNetにはSUMOへのリンクが収録されており、日本語->WordNet->SUMOという経路で語義を引くことができるので、これもデータベースに登録する。とりあえずここまで。